Warum sind in C # Variablen, die in einem try-Block deklariert sind, in ihrem Umfang beschränkt?

23

Ich möchte die Fehlerbehandlung hinzufügen zu:

var firstVariable = 1;
var secondVariable = firstVariable;

Das Folgende wird nicht kompiliert:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Warum muss ein try catch -Block den Gültigkeitsbereich von Variablen wie andere Codeblöcke beeinflussen? 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?

JᴀʏMᴇᴇ
quelle
14
A try.. catchist 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.
Neil
msgstr "ist eine bestimmte Art von Codeblock". Spezifisch auf welche Weise? Danke
J --Mᴀʏ
7
Ich meine, alles zwischen geschweiften Klammern ist ein Codeblock. Sie sehen es nach einer if-Anweisung und nach einer for-Anweisung, obwohl das Konzept dasselbe ist. Der Inhalt befindet sich in einem gegenüber dem übergeordneten Bereich erhöhten Bereich. Ich bin mir ziemlich sicher, dass dies problematisch wäre, wenn Sie nur die geschweiften Klammern verwenden würden, {}ohne es zu versuchen.
Neil
Tipp: Beachten Sie, dass die Verwendung von (IDisposable) {} und nur {} ebenfalls gleichermaßen gilt. Wenn Sie eine Verwendung mit einem IDisposable verwenden, werden die Ressourcen unabhängig von Erfolg oder Misserfolg automatisch bereinigt. Es gibt ein paar Ausnahmen, wie nicht alle Klassen, von denen Sie erwarten würden, dass sie IDisposable implementieren ...
Julia McGuigan
1
Viele Diskussionen zu dieser Frage auf StackOverflow, hier: stackoverflow.com/questions/94977/…
Jon Schneider

Antworten:

91

Was ist, wenn Ihr Code war:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Jetzt würden Sie versuchen, eine nicht deklarierte Variable ( firstVariable) zu verwenden, wenn Ihr Methodenaufruf ausgelöst wird.

Hinweis : Das obige Beispiel beantwortet speziell die ursprüngliche Frage, die "Konsistenz halber" lautet. Dies zeigt, dass es andere Gründe als die Konsistenz gibt. Aber wie Peters Antwort zeigt, gibt es auch ein schlagkräftiges Argument aus der Konsequenz, das mit Sicherheit ein sehr wichtiger Faktor bei der Entscheidung gewesen wäre.

Ben Aaronson
quelle
Ahh, das ist genau das, wonach ich gesucht habe. Ich wusste, dass es einige Sprachfunktionen gab, die das, was ich vorschlug, unmöglich machten, aber ich konnte mir keine Szenarien ausdenken. Vielen Dank.
J 19M at
1
"Jetzt würden Sie versuchen, eine nicht deklarierte Variable zu verwenden, wenn Ihr Methodenaufruf ausgelöst wird." Angenommen, dies wurde vermieden, indem die Variable so behandelt wurde, als ob sie vor dem möglicherweise auszulösenden Code deklariert, aber nicht initialisiert worden wäre. Dann wäre es nicht nicht deklariert, aber es wäre möglicherweise noch nicht zugewiesen, und eine eindeutige Zuweisungsanalyse würde das Lesen seines Werts verbieten (ohne eine dazwischen liegende Zuweisung, die nachweisen könnte, dass sie aufgetreten ist).
Eliah Kagan
3
Für eine statische Sprache wie C # ist die Deklaration nur zur Kompilierungszeit relevant. Der Compiler könnte die Deklaration leicht früher in den Geltungsbereich verschieben. Wichtiger zur Laufzeit ist, dass die Variable möglicherweise nicht initialisiert wird .
jpmc26
3
Ich bin mit dieser Antwort nicht einverstanden. In C # gibt es bereits die Regel, dass nicht initialisierte Variablen mit einigem Datenflussbewusstsein nicht gelesen werden können. (Versuchen Sie, Variablen in Fällen von a zu deklarieren switchund in anderen auf sie zuzugreifen.) Diese Regel könnte hier leicht zutreffen und verhindern, dass dieser Code trotzdem kompiliert wird. Ich denke, Peters Antwort unten ist plausibler.
Sebastian Redl
2
Es gibt einen Unterschied zwischen nicht deklarierten und nicht initialisierten C # -Titeln. Wenn Sie eine Variable außerhalb des Blocks verwenden dürften, in dem sie deklariert wurde, würde dies bedeuten, dass Sie sie im ersten catchBlock zuweisen könnten und dann definitiv im zweiten tryBlock.
Svick
64

Ich weiß, dass dies von Ben gut beantwortet wurde, aber ich wollte auf die Konsistenz-POV eingehen, die bequemerweise beiseite geschoben wurde. Angenommen, die try/catchBlöcke hätten keinen Einfluss auf den Gültigkeitsbereich, dann würden Sie Folgendes erhalten:

{
    // new scope here
}

try
{
   // Not new scope
}

Und für mich trifft dies direkt auf das Prinzip des geringsten Erstaunens (POLA) zu, weil Sie jetzt die doppelte Pflicht haben {und dies }tun, je nachdem, in welchem ​​Kontext sie vorangegangen sind.

Der einzige Ausweg aus diesem Durcheinander besteht darin, einen anderen Marker zu bestimmen, um try/catchBlöcke abzugrenzen . Das fängt an, einen Codegeruch hinzuzufügen. Zu dem Zeitpunkt, an dem Sie try/catchdie Sprache noch nicht beherrscht haben, wäre es so schlimm gewesen, dass Sie mit der Version mit Geltungsbereich besser dran gewesen wären.

Peter M
quelle
Eine weitere hervorragende Antwort. Und ich hatte noch nie von POLA gehört, eine so schöne Lektüre. Vielen Dank Kumpel.
19.
"Der einzige Ausweg aus diesem Durcheinander besteht darin, einen anderen Marker für die Abgrenzung try/ catchBlockierung zu bestimmen ." - meinen Sie: try { { // scope } }? :)
CompuChip
@CompuChip, das {}je nach Kontext immer noch doppelte Pflicht als Geltungsbereich und nicht als Geltungsbereich für die Erstellung haben würde . try^ //no-scope ^wäre ein Beispiel für einen anderen Marker.
Leliel,
1
Meiner Meinung nach ist dies der wesentlich grundlegendere Grund und näher an der "echten" Antwort.
Jack Aidley
@JackAidley, zumal Sie bereits Code schreiben können, in dem Sie eine Variable verwenden, die möglicherweise nicht zugewiesen ist. Während Bens Antwort einen Punkt darüber enthält, wie nützlich dieses Verhalten ist, sehe ich es nicht als Grund, warum das Verhalten existiert. Bens Antwort sieht, dass das OP "der Beständigkeit zuliebe" sagt, aber Beständigkeit ist ein guter Grund! Ein enger Anwendungsbereich bietet viele weitere Vorteile.
Kat
21

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 IOExceptionder Compiler nicht garantieren konnte, dass die aufgerufene Funktion keine ausgelöst hat , konnte er nicht wissen, dass diese nzugewiesen 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 finallyBlock 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 firstVariableund secondVariablein 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 firstVariableund 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 DieMethode mit einem messageParameter 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.

Eliah Kagan
quelle
"Wenn Sie Code in einen try-Block mit catch-Klauseln schreiben, teilen Sie dem Compiler und jedem menschlichen Leser mit, dass er so behandelt werden sollte, als ob möglicherweise nicht alle ausgeführt werden könnten." Der Compiler kümmert sich darum, dass die Steuerung auch dann zu späteren Anweisungen gelangen kann, wenn vorherige Anweisungen eine Ausnahme auslösen. Der Compiler geht normalerweise davon aus, dass, wenn eine Anweisung eine Ausnahme auslöst, die nächste Anweisung nicht ausgeführt wird, sodass eine nicht zugewiesene Variable nicht gelesen wird. Wenn Sie einen 'catch' hinzufügen, kann die Steuerung zu späteren Anweisungen gelangen - es kommt auf den catch an, nicht darauf, ob der Code im try-Block ausgelöst wird.
Pete Kirkham