Wäre es aus Gründen der Konsistenz nicht sinnvoll, wenn wir unseren Code mit Fehlerbehandlung verpacken könnten, ohne ihn umgestalten zu müssen?
Um dies zu beantworten, muss man sich mehr als nur den Gültigkeitsbereich einer Variablen ansehen .
Selbst wenn die Variable im Gültigkeitsbereich verbleiben würde, würde sie nicht definitiv zugewiesen .
Wenn Sie die Variable im try-Block deklarieren, bedeutet dies für den Compiler und für den menschlichen Leser, dass sie nur innerhalb dieses Blocks von Bedeutung ist. Es ist nützlich, wenn der Compiler dies erzwingt.
Wenn die Variable nach dem try-Block im Gültigkeitsbereich sein soll, können Sie sie außerhalb des Blocks deklarieren:
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);
}
Dies drückt aus, dass die Variable möglicherweise außerhalb des try-Blocks von Bedeutung ist. Der Compiler wird dies zulassen.
Es zeigt aber auch einen anderen Grund, warum es normalerweise nicht sinnvoll ist, Variablen im Gültigkeitsbereich zu belassen, nachdem sie in einen try-Block eingefügt wurden. Der C # -Compiler führt eine eindeutige Zuweisungsanalyse durch und verhindert, dass der Wert einer Variablen gelesen wird, für die er keinen Wert angegeben hat. Sie können also immer noch nicht aus der Variablen lesen.
Angenommen, ich versuche, nach dem try-Block aus der Variablen zu lesen:
Console.WriteLine(firstVariable);
Das wird einen Kompilierungsfehler geben :
CS0165 Verwendung der nicht zugewiesenen lokalen Variablen 'firstVariable'
Ich habe Environment.Exit im catch-Block aufgerufen , damit ich weiß, dass die Variable vor dem Aufruf von Console.WriteLine zugewiesen wurde. Der Compiler leitet dies jedoch nicht ab.
Warum ist der Compiler so streng?
Ich kann das nicht einmal tun:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
Eine Möglichkeit, diese Einschränkung zu betrachten, besteht darin, zu sagen, dass die Analyse der eindeutigen Zuweisung in C # nicht sehr komplex ist. Eine andere Sichtweise ist jedoch, dass Sie, wenn Sie Code in einen try-Block mit catch-Klauseln schreiben, sowohl dem Compiler als auch menschlichen Lesern mitteilen, dass er so behandelt werden soll, als ob möglicherweise nicht alle ausgeführt werden können.
Um zu veranschaulichen, was ich meine, stellen Sie sich vor, der Compiler hätte den obigen Code zugelassen, aber Sie haben dann einen Aufruf im try-Block zu einer Funktion hinzugefügt , von der Sie persönlich wissen, dass sie keine Ausnahme auslöst . Da IOException
der Compiler nicht garantieren konnte, dass die aufgerufene Funktion keine ausgelöst hat , konnte er nicht wissen, dass diese n
zugewiesen wurde, und dann müssten Sie eine Umgestaltung durchführen.
Dies bedeutet, dass Sie durch den Verzicht auf eine hochentwickelte Analyse bei der Bestimmung, ob eine in einem try-Block mit catch-Klauseln zugewiesene Variable endgültig zugewiesen wurde, vermeiden können, dass später möglicherweise fehlerhafter Code geschrieben wird. (Wenn Sie eine Ausnahme abfangen, denken Sie normalerweise, dass eine geworfen wird.)
Sie können sicherstellen, dass die Variable über alle Codepfade zugewiesen wird.
Sie können den Code kompilieren lassen, indem Sie der Variablen vor dem try-Block oder im catch-Block einen Wert zuweisen. Auf diese Weise wurde es immer noch initialisiert oder zugewiesen, auch wenn die Zuweisung im try-Block nicht erfolgt. Beispielsweise:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Oder:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
Die kompilieren. Es ist jedoch am besten, so etwas nur dann zu tun, wenn der von Ihnen angegebene Standardwert * Sinn ergibt und ein korrektes Verhalten erzeugt.
Beachten Sie, dass Sie in diesem zweiten Fall, in dem Sie die Variable im try-Block und in allen catch-Blöcken zuweisen, obwohl Sie die Variable nach dem try-catch lesen können, die Variable in einem angehängten finally
Block immer noch nicht lesen können , weil Die Ausführung kann in mehr Situationen einen Try-Block hinterlassen, als wir oft denken .
* Übrigens erlauben einige Sprachen, wie C und C ++, nicht initialisierte Variablen und haben keine eindeutige Zuweisungsanalyse, um das Lesen von ihnen zu verhindern. Da das Lesen von nicht initialisiertem Speicher dazu führt, dass sich Programme nicht deterministisch und unberechenbar verhalten , wird generell empfohlen , keine Variablen in diesen Sprachen einzufügen, ohne einen Initialisierer bereitzustellen. In Sprachen mit eindeutiger Zuweisungsanalyse wie C # und Java erspart Ihnen der Compiler das Lesen nicht initialisierter Variablen und das geringere Übel, sie mit bedeutungslosen Werten zu initialisieren, die später als bedeutungslos interpretiert werden können.
Sie können festlegen, dass Codepfade, bei denen die Variable nicht zugewiesen ist, eine Ausnahme auslösen (oder zurückgeben).
Wenn Sie vorhaben, eine Aktion (z. B. Protokollierung) auszuführen und die Ausnahme erneut auszulösen oder eine andere Ausnahme auszulösen, und dies in allen catch-Klauseln geschieht, in denen die Variable nicht zugewiesen ist, weiß der Compiler, dass die Variable zugewiesen wurde:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
Das kompiliert und kann durchaus eine vernünftige Wahl sein. Jedoch in einer tatsächlichen Anwendung, es sei denn , die Ausnahme nur in geworfen Situationen , in denen es nicht einmal sinnvoll ist, zu versuchen , sich zu erholen * , sollten Sie sicherstellen, dass Sie immer noch fangen und richtig es Handhabung irgendwo .
(Sie können die Variable in einem finally-Block auch in dieser Situation nicht lesen, aber es fühlt sich nicht so an, als ob Sie dazu in der Lage wären. Schließlich werden finally-Blöcke im Wesentlichen immer ausgeführt, und in diesem Fall wird die Variable nicht immer zugewiesen .)
* Zum Beispiel haben viele Anwendungen keine catch-Klausel, die eine OutOfMemoryException behandelt, da alles, was sie dagegen tun könnten, mindestens so schlimm wie ein Absturz sein könnte .
Vielleicht sind Sie wirklich tun wollen den Code Refactoring.
In Ihrem Beispiel führen Sie firstVariable
und secondVariable
in try-Blöcken ein. Wie ich bereits sagte, können Sie sie vor den Try-Blöcken definieren, in denen sie zugewiesen sind, damit sie danach im Gültigkeitsbereich bleiben. Sie können den Compiler dazu bringen, aus ihnen zu lesen, indem Sie sicherstellen, dass sie immer zugewiesen sind.
Der Code, der nach diesen Blöcken erscheint, hängt jedoch vermutlich davon ab, ob sie richtig zugewiesen wurden. Wenn dies der Fall ist, sollte Ihr Code dies widerspiegeln und sicherstellen.
Können (und sollten) Sie den Fehler dort tatsächlich behandeln? Einer der Gründe für die Ausnahmebehandlung besteht darin, die Behandlung von Fehlern dort zu vereinfachen, wo sie effektiv gehandhabt werden können , auch wenn dies nicht in der Nähe des Ortes liegt, an dem sie auftreten.
Wenn Sie den Fehler in der Funktion, die diese Variablen initialisiert und verwendet, tatsächlich nicht behandeln können, sollte sich der try-Block möglicherweise überhaupt nicht in dieser Funktion befinden, sondern irgendwo höher (dh in Code, der diese Funktion aufruft, oder Code) das nennt den Code). Stellen Sie nur sicher, dass Sie nicht versehentlich eine Ausnahme abfangen, die an einer anderen Stelle ausgelöst wurde, und dass diese beim Initialisieren von firstVariable
und fälschlicherweise ausgelöst wurde secondVariable
.
Ein anderer Ansatz besteht darin, den Code, der die Variablen verwendet, in den try-Block einzufügen. Das ist oft vernünftig. Wenn dieselben Ausnahmen, die Sie von ihren Initialisierern abfangen, auch vom umgebenden Code ausgelöst werden könnten, sollten Sie sicherstellen, dass Sie diese Möglichkeit beim Umgang mit ihnen nicht vernachlässigen.
(Ich gehe davon aus, dass Sie die Variablen mit Ausdrücken initialisieren, die komplizierter sind als in Ihren Beispielen gezeigt, so dass sie tatsächlich eine Ausnahme auslösen können, und dass Sie nicht wirklich vorhaben , alle möglichen Ausnahmen abzufangen , sondern nur bestimmte Ausnahmen abzufangen Sie können antizipieren und nach Bedeutung zu behandeln . Es stimmt , dass die reale Welt so schön , nicht immer und Produktionscode manchmal tut dies , aber da Ihr Ziel ist es, Fehler zu behandeln , die auftreten , während zwei spezifische Variablen zu initialisieren, werden alle catch - Klauseln schreiben Sie für diesen speziellen Zweck sollte spezifisch für die Fehler sein, die das sind.)
Eine dritte Möglichkeit besteht darin , den Code, der fehlschlagen kann, und den Try-Catch, der ihn verarbeitet, in eine eigene Methode zu extrahieren . Dies ist nützlich, wenn Sie Fehler zuerst vollständig beheben und sich dann nicht darum kümmern möchten, versehentlich eine Ausnahme abzufangen, die stattdessen an einer anderen Stelle behandelt werden sollte.
Angenommen, Sie möchten die Anwendung sofort beenden, wenn keine der Variablen zugewiesen wurde. (Offensichtlich sind nicht alle Ausnahmebehandlungen für schwerwiegende Fehler vorgesehen. Dies ist nur ein Beispiel. Möglicherweise möchten Sie, dass Ihre Anwendung auf das Problem reagiert, oder auch nicht.)
// 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...
}
Dieser Code gibt ein ValueTuple mit der Syntax von C # 7.0 zurück und dekonstruiert es , um mehrere Werte zurückzugeben. Wenn Sie sich jedoch noch in einer früheren Version von C # befinden, können Sie diese Technik weiterhin verwenden. Sie können beispielsweise Parameter verwenden oder ein benutzerdefiniertes Objekt zurückgeben, das beide Werte bereitstellt . Wenn die beiden Variablen nicht eng miteinander verbunden sind, ist es wahrscheinlich besser , ohnehin zwei separate Methoden zu verwenden.
Insbesondere wenn Sie über mehrere Methoden wie diese verfügen, sollten Sie Ihren Code zentralisieren, um den Benutzer über schwerwiegende Fehler zu informieren und das Programm zu beenden. (Sie könnten beispielsweise eine Die
Methode mit einem message
Parameter schreiben .) Die throw new InvalidOperationException();
Zeile wird nie tatsächlich ausgeführt, sodass Sie keine catch-Klausel dafür schreiben müssen (und sollten).
Abgesehen vom Beenden, wenn ein bestimmter Fehler auftritt, können Sie manchmal Code schreiben, der so aussieht, wenn Sie eine Ausnahme eines anderen Typs auslösen, der die ursprüngliche Ausnahme umschließt . (In dieser Situation benötigen Sie keinen zweiten, nicht erreichbaren Wurfausdruck.)
Fazit: Scope ist nur ein Teil des Bildes.
Sie können den Effekt erzielen, dass Ihr Code mit Fehlerbehandlung ohne Refactoring (oder, wenn Sie es vorziehen, mit kaum Refactoring) umbrochen wird, indem Sie einfach die Deklarationen der Variablen von ihren Zuweisungen trennen. Der Compiler lässt dies zu, wenn Sie die definierten Zuweisungsregeln von C # erfüllen und eine Variable vor dem try-Block deklarieren, um den größeren Gültigkeitsbereich zu verdeutlichen. Ein weiteres Refactoring ist jedoch möglicherweise immer noch die beste Option.
try.. catch
ist ein bestimmter Codeblocktyp, und soweit alle Codeblöcke vorhanden sind, können Sie eine Variable in einer nicht deklarieren und dieselbe Variable in einer anderen aus Gründen des Gültigkeitsbereichs verwenden.