Warum werden Variablen nicht in "try" im Gültigkeitsbereich von "catch" oder "finally" deklariert?


139

In C # und in Java (und möglicherweise auch in anderen Sprachen) sind in einem "try" -Block deklarierte Variablen in den entsprechenden "catch" - oder "finally" -Blöcken nicht im Gültigkeitsbereich. Der folgende Code wird beispielsweise nicht kompiliert:

try {
  String s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

In diesem Code tritt ein Kompilierungsfehler bei der Referenz auf s im catch-Block auf, da s nur im try-Block im Gültigkeitsbereich liegt. (In Java lautet der Kompilierungsfehler "s kann nicht behoben werden"; in C # heißt es "Der Name 's' existiert im aktuellen Kontext nicht".)

Die allgemeine Lösung für dieses Problem scheint darin zu bestehen, Variablen unmittelbar vor dem try-Block und nicht innerhalb des try-Blocks zu deklarieren:

String s;
try {
  s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

Zumindest für mich fühlt sich dies jedoch (1) wie eine klobige Lösung an, und (2) es führt dazu, dass die Variablen einen größeren Umfang haben als vom Programmierer beabsichtigt (der gesamte Rest der Methode, anstatt nur im Kontext der try-catch-finally).

Meine Frage ist, was waren / sind die Gründe für diese Entscheidung zum Sprachdesign (in Java, in C # und / oder in anderen anwendbaren Sprachen)?

Antworten:


171

Zwei Dinge:

  1. Im Allgemeinen hat Java nur zwei Ebenen: globale und funktionale. Try / Catch ist jedoch eine Ausnahme (kein Wortspiel beabsichtigt). Wenn eine Ausnahme ausgelöst wird und dem Ausnahmeobjekt eine Variable zugewiesen wird, ist diese Objektvariable nur im Abschnitt "catch" verfügbar und wird zerstört, sobald der catch abgeschlossen ist.

  2. (und noch wichtiger). Sie können nicht wissen, wo im try-Block die Ausnahme ausgelöst wurde. Möglicherweise wurde Ihre Variable deklariert. Daher ist es unmöglich zu sagen, welche Variablen für die catch / finally-Klausel verfügbar sein werden. Stellen Sie sich den folgenden Fall vor, in dem der Umfang dem von Ihnen vorgeschlagenen entspricht:

    
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s = "blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }

Dies ist eindeutig ein Problem. Wenn Sie den Ausnahmebehandler erreichen, wurde s nicht deklariert. Angesichts der Tatsache, dass Catches außergewöhnliche Umstände behandeln sollen und schließlich ausgeführt werden müssen , ist es weitaus besser als zur Laufzeit, sicher zu sein und dies zur Kompilierungszeit als Problem zu deklarieren.


55

Wie können Sie sicher sein, dass Sie den Deklarationsteil in Ihrem Fangblock erreicht haben? Was ist, wenn die Instanziierung die Ausnahme auslöst?


6
Huh? Variablendeklarationen lösen keine Ausnahmen aus.
Joshua

6
Einverstanden ist es die Instanziierung, die die Ausnahme auslösen könnte.
Burkhard

19

In Sprachen im C-Stil bleibt das, was in den geschweiften Klammern passiert, traditionell in den geschweiften Klammern. Ich denke, dass die Lebensdauer einer variablen Ausdehnung über solche Bereiche für die meisten Programmierer nicht intuitiv wäre. Sie können erreichen, was Sie wollen, indem Sie die Blöcke try / catch / finally in eine andere Ebene von Klammern einschließen. z.B

... code ...
{
    string s = "test";
    try
    {
        // more code
    }
    catch(...)
    {
        Console.Out.WriteLine(s);
    }
}

EDIT: Ich denke, jede Regel hat eine Ausnahme. Folgendes ist in C ++ gültig:

int f() { return 0; }

void main() 
{
    int y = 0;

    if (int x = f())
    {
        cout << x;
    }
    else
    {
        cout << x;
    }
}

Der Geltungsbereich von x ist die Bedingungsklausel, die then-Klausel und die else-Klausel.


10

Alle anderen haben die Grundlagen angesprochen - was in einem Block passiert, bleibt in einem Block. Im Fall von .NET kann es jedoch hilfreich sein, zu untersuchen, was der Compiler für möglich hält. Nehmen Sie zum Beispiel den folgenden Try / Catch-Code (beachten Sie, dass der StreamReader außerhalb der Blöcke korrekt deklariert ist):

static void TryCatchFinally()
{
    StreamReader sr = null;
    try
    {
        sr = new StreamReader(path);
        Console.WriteLine(sr.ReadToEnd());
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        if (sr != null)
        {
            sr.Close();
        }
    }
}

Dies wird zu etwas ähnlichem wie dem in MSIL kompiliert:

.method private hidebysig static void  TryCatchFinallyDispose() cil managed
{
  // Code size       53 (0x35)    
  .maxstack  2    
  .locals init ([0] class [mscorlib]System.IO.StreamReader sr,    
           [1] class [mscorlib]System.Exception ex)    
  IL_0000:  ldnull    
  IL_0001:  stloc.0    
  .try    
  {    
    .try    
    {    
      IL_0002:  ldsfld     string UsingTest.Class1::path    
      IL_0007:  newobj     instance void [mscorlib]System.IO.StreamReader::.ctor(string)    
      IL_000c:  stloc.0    
      IL_000d:  ldloc.0    
      IL_000e:  callvirt   instance string [mscorlib]System.IO.TextReader::ReadToEnd()
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0018:  leave.s    IL_0028
    }  // end .try
    catch [mscorlib]System.Exception 
    {
      IL_001a:  stloc.1
      IL_001b:  ldloc.1    
      IL_001c:  callvirt   instance string [mscorlib]System.Exception::ToString()    
      IL_0021:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0026:  leave.s    IL_0028    
    }  // end handler    
    IL_0028:  leave.s    IL_0034    
  }  // end .try    
  finally    
  {    
    IL_002a:  ldloc.0    
    IL_002b:  brfalse.s  IL_0033    
    IL_002d:  ldloc.0    
    IL_002e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()    
    IL_0033:  endfinally    
  }  // end handler    
  IL_0034:  ret    
} // end of method Class1::TryCatchFinallyDispose

Was sehen wir? MSIL respektiert die Blöcke - sie sind Teil des zugrunde liegenden Codes, der beim Kompilieren Ihres C # generiert wird. Der Bereich ist nicht nur in der C # -Spezifikation festgelegt, sondern auch in der CLR- und CLS-Spezifikation.

Der Bereich schützt Sie, aber Sie müssen ihn gelegentlich umgehen. Mit der Zeit gewöhnt man sich daran und es fühlt sich natürlich an. Wie alle anderen sagten, bleibt das, was in einem Block passiert, in diesem Block. Sie möchten etwas teilen? Sie müssen außerhalb der Blöcke gehen ...


8

In C ++ ist der Umfang einer automatischen Variablen jedenfalls durch die sie umgebenden geschweiften Klammern begrenzt. Warum sollte jemand erwarten, dass dies anders ist, wenn er ein try-Schlüsselwort außerhalb der geschweiften Klammern setzt?


1
Einverstanden; "}" bedeutet das Ende des Gültigkeitsbereichs. Allerdings versuchen-catch-finally ungewöhnlich ist, dass nach einem try - Block, Sie müssen einen Haken und / oder schließlich blockieren; eine Ausnahme von der normalen Regel, bei der der Umfang eines Try-Blocks, der in den zugehörigen catch / finally übertragen wird, akzeptabel erscheint?
Jon Schneider

7

Wie Ravenspoint hervorhob, erwartet jeder, dass Variablen lokal für den Block sind, in dem er definiert ist. Er tryführt einen Block ein und tut dies auch catch.

Wenn Sie lokale Variablen für beide tryund möchten catch, versuchen Sie, beide in einen Block einzuschließen:

// here is some code
{
    string s;
    try
    {

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}

5

Die einfache Antwort lautet, dass C und die meisten Sprachen, die seine Syntax geerbt haben, einen Blockbereich haben. Das heißt, wenn eine Variable in einem Block definiert ist, dh innerhalb von {}, ist dies ihr Gültigkeitsbereich.

Die Ausnahme ist übrigens JavaScript, das eine ähnliche Syntax hat, aber einen Funktionsumfang aufweist. In JavaScript befindet sich eine in einem try-Block deklarierte Variable im catch-Block und überall in der enthaltenen Funktion im Gültigkeitsbereich.


4

@burkhard hat die Frage, warum richtig beantwortet wurde, aber als Hinweis wollte ich hinzufügen, dass Ihr empfohlenes Lösungsbeispiel zwar 99,9999 +% der Zeit gut ist, es jedoch keine gute Praxis ist, es weitaus sicherer ist, vor der Verwendung entweder auf Null zu prüfen Innerhalb des try-Blocks wird etwas instanziiert oder die Variable mit etwas initialisiert, anstatt sie nur vor dem try-Block zu deklarieren. Beispielsweise:

string s = String.Empty;
try
{
    //do work
}
catch
{
   //safely access s
   Console.WriteLine(s);
}

Oder:

string s;
try
{
    //do work
}
catch
{
   if (!String.IsNullOrEmpty(s))
   {
       //safely access s
       Console.WriteLine(s);
   }
}

Dies sollte Skalierbarkeit in der Problemumgehung bieten, sodass Sie auch dann sicher auf die Daten aus Ihrem catch-Block zugreifen können sollten, wenn das, was Sie im try-Block tun, komplexer ist als das Zuweisen einer Zeichenfolge.


4

Gemäß dem Abschnitt " Auslösen und Abfangen von Ausnahmen" in Lektion 2 des MCTS Self-Paced Training Kit (Prüfung 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation liegt der Grund möglicherweise darin, dass die Ausnahme aufgetreten ist vor Variablendeklarationen im try-Block (wie andere bereits bemerkt haben).

Zitat von Seite 25:

"Beachten Sie, dass die StreamReader-Deklaration im vorherigen Beispiel außerhalb des Try-Blocks verschoben wurde. Dies ist erforderlich, da der finally-Block nicht auf Variablen zugreifen kann, die im Try-Block deklariert sind. Dies ist sinnvoll, da je nach dem, wo eine Ausnahme aufgetreten ist, Variablendeklarationen innerhalb des Der Try-Block wurde möglicherweise noch nicht ausgeführt . "


4

Die Antwort lautet, wie alle betont haben, so ziemlich "so werden Blöcke definiert".

Es gibt einige Vorschläge, um den Code schöner zu machen. Siehe ARM

 try (FileReader in = makeReader(), FileWriter out = makeWriter()) {
       // code using in and out
 } catch(IOException e) {
       // ...
 }

Schließungen sollen dies ebenfalls ansprechen.

with(FileReader in : makeReader()) with(FileWriter out : makeWriter()) {
    // code using in and out
}

UPDATE: ARM ist in Java 7 implementiert. Http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html


2

Ihre Lösung ist genau das, was Sie tun sollten. Sie können nicht sicher sein, ob Ihre Deklaration überhaupt im try-Block erreicht wurde, was zu einer weiteren Ausnahme im catch-Block führen würde.

Es muss einfach als separater Bereich funktionieren.

try
    dim i as integer = 10 / 0 ''// Throw an exception
    dim s as string = "hi"
catch (e)
    console.writeln(s) ''// Would throw another exception, if this was allowed to compile
end try

2

Die Variablen sind auf Blockebene und auf diesen Try- oder Catch-Block beschränkt. Ähnlich wie beim Definieren einer Variablen in einer if-Anweisung. Denken Sie an diese Situation.

try {    
    fileOpen("no real file Name");    
    String s = "GO TROJANS"; 
} catch (Exception) {   
    print(s); 
}

Der String würde niemals deklariert werden, daher kann er nicht abhängig gemacht werden.


2

Weil der try-Block und der catch-Block zwei verschiedene Blöcke sind.

Würden Sie im folgenden Code erwarten, dass in Block A definierte s in Block B sichtbar sind?

{ // block A
  string s = "dude";
}

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}

2

Während es in Ihrem Beispiel seltsam ist, dass es nicht funktioniert, nehmen Sie dieses ähnliche:

    try
    {
         //Code 1
         String s = "1|2";
         //Code 2
    }
    catch
    {
         Console.WriteLine(s.Split('|')[1]);
    }

Dies würde dazu führen, dass der Fang eine Nullreferenzausnahme auslöst, wenn Code 1 fehlerhaft ist. Obwohl die Semantik von try / catch ziemlich gut verstanden ist, wäre dies ein ärgerlicher Eckfall, da s mit einem Anfangswert definiert ist, so dass es theoretisch niemals null sein sollte, aber unter gemeinsamer Semantik wäre es.

Auch dies könnte theoretisch behoben werden, indem nur getrennte Definitionen ( String s; s = "1|2";) oder andere Bedingungen zugelassen werden, aber es ist im Allgemeinen einfacher, einfach Nein zu sagen.

Darüber hinaus kann die Semantik des Gültigkeitsbereichs ausnahmslos global definiert werden. Insbesondere halten die Einheimischen {}in allen Fällen so lange, wie sie definiert sind. Kleiner Punkt, aber ein Punkt.

Um das zu tun, was Sie wollen, können Sie schließlich eine Reihe von Klammern um den Try-Catch setzen. Gibt Ihnen den gewünschten Umfang, obwohl dies auf Kosten einer geringen Lesbarkeit geht, aber nicht zu viel.

{
     String s;
     try
     {
          s = "test";
          //More code
     }
     catch
     {
          Console.WriteLine(s);
     }
}

1

In dem von Ihnen angegebenen Beispiel kann das Initialisieren von s keine Ausnahme auslösen. Sie würden also denken, dass der Anwendungsbereich möglicherweise erweitert werden könnte.

Im Allgemeinen können Initialisiererausdrücke jedoch Ausnahmen auslösen. Es wäre nicht sinnvoll, wenn eine Variable, deren Initialisierer eine Ausnahme ausgelöst hat (oder die nach einer anderen Variablen deklariert wurde, in der dies geschehen ist), im Bereich für catch / finally liegt.

Auch die Lesbarkeit des Codes würde darunter leiden. Die Regel in C (und den darauf folgenden Sprachen, einschließlich C ++, Java und C #) ist einfach: Variable Bereiche folgen Blöcken.

Wenn Sie möchten, dass sich eine Variable im Bereich von try / catch / finally befindet, aber nirgendwo anders, dann wickeln Sie das Ganze in einen anderen Satz von Klammern (einen bloßen Block) und deklarieren Sie die Variable vor dem Versuch.


1

Ein Grund dafür, dass sie nicht im selben Bereich liegen, ist, dass Sie an jedem Punkt des try-Blocks die Ausnahme ausgelöst haben können. Wenn sie sich im selben Bereich befinden, ist es eine Katastrophe beim Warten, denn je nachdem, wo die Ausnahme ausgelöst wurde, könnte sie noch mehrdeutiger sein.

Zumindest wenn es außerhalb des try-Blocks deklariert ist, wissen Sie sicher, wie hoch die Variable mindestens sein kann, wenn eine Ausnahme ausgelöst wird. Der Wert der Variablen vor dem try-Block.


1

Wenn Sie eine lokale Variable deklarieren, wird sie auf dem Stapel abgelegt (bei einigen Typen befindet sich der gesamte Wert des Objekts auf dem Stapel, bei anderen Typen befindet sich nur eine Referenz auf dem Stapel). Wenn innerhalb eines try-Blocks eine Ausnahme vorliegt, werden die lokalen Variablen innerhalb des Blocks freigegeben, was bedeutet, dass der Stapel wieder in den Zustand "abgewickelt" wird, in dem er sich zu Beginn des try-Blocks befand. Dies ist beabsichtigt. Auf diese Weise kann try / catch alle Funktionsaufrufe innerhalb des Blocks beenden und Ihr System wieder in einen Funktionszustand versetzen. Ohne diesen Mechanismus könnten Sie niemals sicher sein, ob etwas passiert, wenn eine Ausnahme auftritt.

Es scheint mir ein schlechtes Design zu sein, wenn sich Ihr Fehlerbehandlungscode auf extern deklarierte Variablen stützt, deren Werte innerhalb des try-Blocks geändert wurden. Was Sie tun, ist im Wesentlichen, absichtlich Ressourcen zu verlieren, um Informationen zu erhalten (in diesem speziellen Fall ist es nicht so schlimm, weil Sie nur Informationen verlieren, aber stellen Sie sich vor, es wäre eine andere Ressource? Sie machen sich das Leben in der Welt nur schwerer Zukunft). Ich würde vorschlagen, Ihre Try-Blöcke in kleinere Blöcke aufzuteilen, wenn Sie mehr Granularität bei der Fehlerbehandlung benötigen.


1

Wenn Sie einen Versuch fangen, sollten Sie größtenteils wissen, welche Fehler es werfen könnte. Diese Ausnahmeklassen sagen normalerweise alles, was Sie über die Ausnahme benötigen. Wenn nicht, sollten Sie eigene Ausnahmeklassen erstellen und diese Informationen weitergeben. Auf diese Weise müssen Sie die Variablen niemals aus dem try-Block abrufen, da die Ausnahme selbsterklärend ist. Wenn Sie dies also viel tun müssen, denken Sie über Ihr Design nach und versuchen Sie zu überlegen, ob es eine andere Möglichkeit gibt, entweder das Kommen von Ausnahmen vorherzusagen oder die aus den Ausnahmen kommenden Informationen zu verwenden und dann möglicherweise Ihre eigenen erneut zu werfen Ausnahme mit mehr Informationen.


1

Wie bereits von anderen Benutzern erwähnt, definieren die geschweiften Klammern den Gültigkeitsbereich in nahezu jeder mir bekannten Sprache im C-Stil.

Wenn es sich um eine einfache Variable handelt, warum interessiert es Sie dann, wie lange sie im Geltungsbereich sein wird? Es ist keine so große Sache.

Wenn es sich in C # um eine komplexe Variable handelt, möchten Sie IDisposable implementieren. Sie können dann entweder try / catch / finally verwenden und obj.Dispose () im finally-Block aufrufen. Oder Sie können das Schlüsselwort using verwenden, das am Ende des Codeabschnitts automatisch Dispose aufruft.


1

In Python sind sie in den catch / finally-Blöcken sichtbar, wenn die sie deklarierende Zeile nicht geworfen hat.


1

Was ist, wenn die Ausnahme in einem Code ausgelöst wird, der über der Deklaration der Variablen liegt? Das heißt, die Erklärung selbst war in diesem Fall nicht zustande gekommen.

try {

       //doSomeWork // Exception is thrown in this line. 
       String s;
       //doRestOfTheWork

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}

1

In der C # -Spezifikation (15.2) heißt es: "Der Gültigkeitsbereich einer in einem Block deklarierten lokalen Variablen oder Konstante ist der Block."

(In Ihrem ersten Beispiel ist der try-Block der Block, in dem "s" deklariert ist.)


0

Mein Gedanke wäre, dass, weil etwas im try-Block die Ausnahme ausgelöst hat, dessen Namespace-Inhalt nicht vertrauenswürdig ist. Wenn Sie also auf die Zeichenfolgen im catch-Block verweisen, kann dies zu einer weiteren Ausnahme führen.


0

Wenn es keinen Kompilierungsfehler auslöst und Sie ihn für den Rest der Methode deklarieren können, gibt es keine Möglichkeit, ihn nur im Versuchsbereich zu deklarieren. Es zwingt Sie dazu, explizit anzugeben, wo die Variable existieren soll, und keine Annahmen zu treffen.


0

Wenn wir das Scoping-Block-Problem für einen Moment ignorieren, müsste der Complier in einer Situation, die nicht genau definiert ist, viel härter arbeiten. Dies ist zwar nicht unmöglich, aber der Scoping-Fehler zwingt Sie, den Autor des Codes, auch, die Implikation des von Ihnen geschriebenen Codes zu erkennen (dass die Zeichenfolge s im catch-Block null sein kann). Wenn Ihr Code legal war, wird s im Fall einer OutOfMemory-Ausnahme nicht einmal garantiert, dass s ein Speicherplatz zugewiesen wird:

// won't compile!
try
{
    VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException
    string s = "Help";
}
catch
{
    Console.WriteLine(s); // whoops!
}

Die CLR (und damit der Compiler) zwingen Sie außerdem, Variablen zu initialisieren, bevor sie verwendet werden. In dem dargestellten Fangblock kann dies nicht garantiert werden.

Am Ende muss der Compiler viel arbeiten, was in der Praxis nicht viel bringt und die Leute wahrscheinlich verwirren und sie dazu bringen würde, zu fragen, warum Try / Catch anders funktioniert.

Zusätzlich zur Konsistenz können der Compiler und die CLR eine bessere Garantie für den Status einer Variablen innerhalb eines Catch-Blocks bieten, indem sie nichts Besonderes zulassen und die bereits etablierte Scoping-Semantik einhalten, die in der gesamten Sprache verwendet wird. Dass es existiert und initialisiert wurde.

Beachten Sie, dass die Sprachdesigner gute Arbeit mit anderen Konstrukten wie Verwenden und Sperren geleistet haben, bei denen das Problem und der Umfang genau definiert sind, sodass Sie klareren Code schreiben können.

zB das Schlüsselwort using mit IDisposable- Objekten in:

using(Writer writer = new Writer())
{
    writer.Write("Hello");
}

ist äquivalent zu:

Writer writer = new Writer();
try
{        
    writer.Write("Hello");
}
finally
{
    if( writer != null)
    {
        ((IDisposable)writer).Dispose();
    }
}

Wenn Ihr Versuch / Fang / Endlich schwer zu verstehen ist, versuchen Sie, eine andere Indirektionsebene mit einer Zwischenklasse umzugestalten oder einzuführen, die die Semantik dessen, was Sie erreichen möchten, zusammenfasst. Ohne echten Code zu sehen, ist es schwierig, genauer zu sein.


0

Anstelle einer lokalen Variablen könnte eine öffentliche Eigenschaft deklariert werden. Dies sollte auch einen weiteren möglichen Fehler einer nicht zugewiesenen Variablen vermeiden. öffentlicher String S {get; einstellen; }}


-1

Wenn die Zuweisungsoperation fehlschlägt, hat Ihre catch-Anweisung einen Nullverweis zurück auf die nicht zugewiesene Variable.


2
Es ist nicht zugewiesen. Es ist nicht einmal null (im Gegensatz zu Instanz- und statischen Variablen).
Tom Hawtin - Tackline

-1

C # 3.0:

string html = new Func<string>(() =>
{
    string webpage;

    try
    {
        using(WebClient downloader = new WebClient())
        {
            webpage = downloader.DownloadString(url);
        }
    }
    catch(WebException)
    {
        Console.WriteLine("Download failed.");  
    }

    return webpage;
})();

WTF? Warum die Abstimmung? Die Kapselung ist ein wesentlicher Bestandteil von OOP. Sieht auch hübsch aus.
Kern

2
Ich war nicht der Downvote, aber was falsch ist, ist die Rückgabe einer nicht initialisierten Zeichenfolge.
Ben Voigt
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.