Mis à part la cohérence, ne serait-il pas logique pour nous de pouvoir envelopper notre code avec la gestion des erreurs sans avoir besoin de refactoriser?
Pour répondre à cela, il est nécessaire de regarder plus que la portée d'une variable .
Même si la variable restait dans la portée, elle ne serait pas définitivement attribuée .
La déclaration de la variable dans le bloc try exprime - au compilateur et aux lecteurs humains - qu'elle n'a de sens qu'à l'intérieur de ce bloc. Il est utile que le compilateur applique cela.
Si vous voulez que la variable soit dans la portée après le bloc try, vous pouvez la déclarer en dehors du bloc:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
Cela exprime que la variable peut être significative en dehors du bloc try. Le compilateur le permettra.
Mais cela montre également une autre raison pour laquelle il ne serait généralement pas utile de conserver les variables dans la portée après les avoir introduites dans un bloc try. Le compilateur C # effectue une analyse d'affectation définie et interdit de lire la valeur d'une variable dont il n'a pas été prouvé qu'elle a reçu une valeur. Vous ne pouvez donc toujours pas lire la variable.
Supposons que j'essaie de lire la variable après le bloc try:
Console.WriteLine(firstVariable);
Cela donnera une erreur de compilation :
CS0165 Utilisation de la variable locale non affectée 'firstVariable'
J'ai appelé Environment.Exit dans le bloc catch, donc je sais que la variable a été assignée avant l'appel à Console.WriteLine. Mais le compilateur ne déduit pas cela.
Pourquoi le compilateur est-il si strict?
Je ne peux même pas faire ça:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
Une façon de considérer cette restriction est de dire que l'analyse d'affectation définitive en C # n'est pas très sophistiquée. Mais une autre façon de voir les choses est que, lorsque vous écrivez du code dans un bloc try avec des clauses catch, vous dites au compilateur et à tous les lecteurs humains qu'il doit être traité comme s'il ne pouvait pas tous s'exécuter.
Pour illustrer ce que je veux dire, imaginez si le compilateur a autorisé le code ci-dessus, mais vous avez ensuite ajouté un appel dans le bloc try à une fonction que vous savez personnellement ne lèvera pas d'exception . N'étant pas en mesure de garantir que la fonction appelée ne lançait pas un IOException
, le compilateur ne pouvait pas savoir que cela n
avait été attribué, et vous devriez alors refactoriser.
Cela signifie qu'en renonçant à une analyse très sophistiquée pour déterminer si une variable affectée dans un bloc try avec des clauses catch a été définitivement affectée par la suite, le compilateur vous aide à éviter d'écrire du code susceptible de se casser plus tard. (Après tout, attraper une exception signifie généralement que vous pensez qu'une peut être levée.)
Vous pouvez vous assurer que la variable est affectée via tous les chemins de code.
Vous pouvez faire compiler le code en donnant à la variable une valeur avant le bloc try ou dans le bloc catch. De cette façon, il aura toujours été initialisé ou affecté, même si l'affectation dans le bloc try n'a pas lieu. Par exemple:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Ou:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
Ceux-ci se compilent. Mais il est préférable de ne faire quelque chose comme ça que si la valeur par défaut que vous lui donnez a du sens * et produit un comportement correct.
Notez que, dans ce deuxième cas où vous affectez la variable dans le bloc try et dans tous les blocs catch, bien que vous puissiez lire la variable après le try-catch, vous ne pourrez toujours pas lire la variable à l'intérieur d'un finally
bloc attaché , car l'exécution peut laisser un bloc d'essai dans plus de situations que nous ne le pensons souvent .
* Soit dit en passant, certains langages, comme C et C ++, autorisent tous deux des variables non initialisées et n'ont pas d'analyse d'affectation définie pour empêcher leur lecture. Étant donné que la lecture de la mémoire non initialisée entraîne le comportement des programmes de manière non déterministe et erratique , il est généralement recommandé d'éviter d'introduire des variables dans ces langues sans fournir d'initialiseur. Dans les langages avec une analyse d'affectation définie comme C # et Java, le compilateur vous évite de lire des variables non initialisées et aussi du moindre mal de les initialiser avec des valeurs dénuées de sens qui peuvent plus tard être mal interprétées comme significatives.
Vous pouvez faire en sorte que les chemins de code où la variable n'est pas affectée lèvent une exception (ou retournent).
Si vous prévoyez d'effectuer une action (comme la journalisation) et de renvoyer l'exception ou de lever une autre exception, et cela se produit dans toutes les clauses catch où la variable n'est pas affectée, le compilateur saura que la variable a été affectée:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
Cela compile, et peut être un choix raisonnable. Cependant, dans une application réelle, à moins que l'exception ne soit levée que dans des situations où cela n'a même pas de sens d'essayer de récupérer * , vous devez vous assurer que vous êtes toujours en train de l'attraper et de le manipuler correctement quelque part .
(Vous ne pouvez pas non plus lire la variable dans un bloc finally dans cette situation, mais il ne semble pas que vous devriez pouvoir - après tout, les blocs finalement fonctionnent toujours essentiellement, et dans ce cas, la variable n'est pas toujours affectée .)
* Par exemple, de nombreuses applications n'ont pas de clause catch qui gère une OutOfMemoryException car tout ce qu'elles pourraient faire à ce sujet pourrait être au moins aussi mauvais qu'un plantage .
Peut-être vous vraiment ne voulez factoriser le code.
Dans votre exemple, vous introduisez firstVariable
et secondVariable
essayez des blocs. Comme je l'ai dit, vous pouvez les définir avant les blocs try dans lesquels ils sont assignés afin qu'ils restent dans la portée par la suite, et vous pouvez satisfaire / tromper le compilateur en vous permettant de lire à partir d'eux en vous assurant qu'ils sont toujours assignés.
Mais le code qui apparaît après ces blocs dépend probablement de leur affectation correcte. Si tel est le cas, votre code doit refléter et garantir cela.
Premièrement, pouvez-vous (et devriez-vous) gérer l'erreur là-bas? L'une des raisons pour lesquelles la gestion des exceptions existe est de faciliter la gestion des erreurs là où elles peuvent être gérées efficacement , même si ce n'est pas près de l'endroit où elles se produisent.
Si vous ne pouvez pas réellement gérer l'erreur dans la fonction qui a initialisé et utilise ces variables, alors peut-être que le bloc try ne devrait pas du tout être dans cette fonction, mais plutôt quelque part plus haut (c'est-à-dire, dans le code qui appelle cette fonction, ou code qui appelle ce code). Assurez-vous simplement que vous n'attrapez pas accidentellement une exception levée ailleurs et supposez à tort qu'elle a été levée lors de l'initialisation firstVariable
et secondVariable
.
Une autre approche consiste à mettre le code qui utilise les variables dans le bloc try. C'est souvent raisonnable. Encore une fois, si les mêmes exceptions que vous attrapez de leurs initialiseurs peuvent également être levées du code environnant, vous devez vous assurer que vous ne négligez pas cette possibilité lors de leur manipulation.
(Je suppose que vous initialisez les variables avec des expressions plus compliquées que celles montrées dans vos exemples, de sorte qu'elles pourraient en fait lever une exception, et aussi que vous ne planifiez pas vraiment de capturer toutes les exceptions possibles , mais simplement de capturer toutes les exceptions spécifiques vous pouvez anticiper et gérer de manière significative . Il est vrai que le monde réel n'est pas toujours aussi agréable et le code de production le fait parfois , mais puisque votre objectif ici est de gérer les erreurs qui se produisent lors de l'initialisation de deux variables spécifiques, toutes les clauses catch que vous écrivez pour ce spécifique Le but doit être spécifique à toutes les erreurs.)
Une troisième façon consiste à extraire le code qui peut échouer et le try-catch qui le gère, dans sa propre méthode. Cela est utile si vous souhaitez d'abord traiter complètement les erreurs, puis ne vous inquiétez pas d'attraper par inadvertance une exception qui devrait être gérée ailleurs à la place.
Supposons, par exemple, que vous souhaitiez quitter immédiatement l'application en cas d'échec de l'affectation de l'une ou l'autre variable. (Évidemment, toute la gestion des exceptions n'est pas destinée aux erreurs fatales; ce n'est qu'un exemple, et peut ou non être la façon dont vous voulez que votre application réagisse au problème.) Vous pourriez donc quelque chose comme ceci:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
Ce code renvoie et déconstruit un ValueTuple avec la syntaxe du C # 7.0 pour renvoyer plusieurs valeurs, mais si vous utilisez toujours une version antérieure de C #, vous pouvez toujours utiliser cette technique; par exemple, vous pouvez utiliser des paramètres ou renvoyer un objet personnalisé qui fournit les deux valeurs . De plus, si les deux variables ne sont pas réellement étroitement liées, il serait probablement préférable d'avoir deux méthodes distinctes de toute façon.
Surtout si vous avez plusieurs méthodes comme celle-ci, vous devriez envisager de centraliser votre code pour avertir l'utilisateur des erreurs fatales et quitter. (Par exemple, vous pouvez écrire unDie
méthode avec un message
paramètre.) La throw new InvalidOperationException();
ligne n'est jamais réellement exécutée , vous n'avez donc pas besoin (et ne devez pas) écrire une clause catch pour elle.
En plus de quitter lorsqu'une erreur particulière se produit, vous pouvez parfois écrire du code qui ressemble à ceci si vous lancez une exception d'un autre type qui encapsule l'exception d'origine . (Dans cette situation, vous n'auriez pas besoin d'une seconde expression de lancement inaccessible.)
Conclusion: la portée n'est qu'une partie de l'image.
Vous pouvez obtenir l'effet d'encapsuler votre code avec une gestion des erreurs sans refactoring (ou, si vous préférez, avec pratiquement aucun refactoring), simplement en séparant les déclarations des variables de leurs affectations. Le compilateur permet cela si vous respectez les règles d'affectation définies de C #, et déclarer une variable avant le bloc try rend sa portée plus claire. Mais la refactorisation peut être toujours votre meilleure option.
try.. catch
est un type spécifique de bloc de code, et dans la mesure où tous les blocs de code vont, vous ne pouvez pas déclarer une variable dans un et utiliser cette même variable dans un autre comme une question de portée.