it-swarm.com.de

Ist der 'endlich'-Teil eines' try ... catch ... finally'-Konstrukts überhaupt notwendig?

Einige Sprachen (wie C++ und frühere Versionen von PHP) unterstützen den finally Teil eines try ... catch ... finally - Konstrukts nicht. Ist finally jemals notwendig? Da der darin enthaltene Code immer ausgeführt wird, warum sollte/sollte ich diesen Code nicht einfach nach einem try ... catch - Block ohne finally -Klausel platzieren? Warum einen verwenden? = (Ich suche nach einem Grund/einer Motivation für die Verwendung/Nichtverwendung von finally, nicht nach einem Grund, 'catch' abzuschaffen oder warum dies legal ist.)

27
Agi Hammerthief

Zusätzlich zu dem, was andere gesagt haben, kann eine Ausnahme auch in die catch-Klausel geworfen werden. Bedenken Sie:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

In diesem Beispiel wird die Funktion Cleanup() nie ausgeführt, da in der catch-Klausel eine Ausnahme ausgelöst wird und der nächsthöhere Fang im Aufrufstapel dies abfängt. Die Verwendung eines finally-Blocks beseitigt dieses Risiko und macht den Code beim Booten sauberer.

38
Erik

Wie bereits erwähnt, gibt es keine Garantie dafür, dass Code nach einer try -Anweisung ausgeführt wird, es sei denn, Sie fangen jede mögliche Ausnahme ab. Das heißt, dies:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

kann umgeschrieben werden1 wie:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

Letzteres erfordert jedoch, dass Sie alle nicht behandelten Ausnahmen abfangen, den Bereinigungscode duplizieren und daran denken, erneut zu werfen. finally ist also nicht notwendig , aber es ist nützlich .

C++ hat finally nicht, weil Bjarne Stroustrup glaubt, dass RAII besser ist oder zumindest für die meisten Fälle ausreicht:

Warum bietet C++ kein "finally" -Konstrukt?

Weil C++ eine Alternative unterstützt, die fast immer besser ist: Die Technik "Ressourcenbeschaffung ist Initialisierung" (TC++ PL3, Abschnitt 14.4). Die Grundidee besteht darin, eine Ressource durch ein lokales Objekt darzustellen, sodass der Destruktor des lokalen Objekts die Ressource freigibt. Auf diese Weise kann der Programmierer nicht vergessen, die Ressource freizugeben.


1 Der spezifische Code zum Abfangen aller Ausnahmen und zum erneuten Auslösen ohne Verlust von Stapelverfolgungsinformationen variiert je nach Sprache. Ich habe Java verwendet, wo der Stack-Trace erfasst wird, wenn die Ausnahme erstellt wird. In C # verwenden Sie einfach throw;.

56
Doval

finally -Blöcke werden normalerweise zum Löschen von Ressourcen verwendet, die die Lesbarkeit verbessern können, wenn mehrere return-Anweisungen verwendet werden:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

vs.

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}
22
AlexFoxGill

Wie Sie anscheinend bereits vermutet haben, bietet C++ ohne diesen Mechanismus dieselben Funktionen. Streng genommen ist der Mechanismus try/finally nicht wirklich notwendig.

Wenn Sie jedoch darauf verzichten, werden einige Anforderungen an die Gestaltung des Restes der Sprache gestellt. In C++ ist derselbe Satz von Aktionen im Destruktor einer Klasse enthalten. Dies funktioniert hauptsächlich (ausschließlich?), Da der Destruktoraufruf in C++ deterministisch ist. Dies führt wiederum zu ziemlich komplexen Regeln über die Lebensdauer von Objekten, von denen einige entschieden nicht intuitiv sind.

Die meisten anderen Sprachen bieten stattdessen eine Form der Speicherbereinigung. Während es Dinge über die Garbage Collection gibt, die umstritten sind (z. B. ihre Effizienz im Vergleich zu anderen Methoden der Speicherverwaltung), ist eines im Allgemeinen nicht so: Der genaue Zeitpunkt, zu dem ein Objekt vom Garbage Collector "bereinigt" wird, ist nicht direkt gebunden zum Umfang des Objekts. Dies verhindert seine Verwendung, wenn die Bereinigung deterministisch sein muss, entweder wenn sie nur für den korrekten Betrieb erforderlich ist, oder wenn Ressourcen so kostbar sind, dass ihre Bereinigung meist nicht willkürlich verzögert wird. try/finally bietet solchen Sprachen eine Möglichkeit, mit Situationen umzugehen, die diese deterministische Bereinigung erfordern.

Ich denke, dass diejenigen, die behaupten, dass die C++ - Syntax für diese Funktion "weniger benutzerfreundlich" ist als die von Java, den Punkt eher verfehlen. Schlimmer noch, es fehlt ihnen ein viel entscheidenderer Punkt in Bezug auf die Aufteilung der Verantwortung, der weit über die Syntax hinausgeht und viel mehr mit der Gestaltung des Codes zu tun hat.

In C++ erfolgt diese deterministische Bereinigung im Destruktor des Objekts. Das heißt, das Objekt kann (und sollte normalerweise) so gestaltet sein, dass es nach sich selbst aufräumt. Dies geht zum Kern des objektorientierten Designs über - eine Klasse sollte so entworfen werden, dass sie eine Abstraktion liefert und ihre eigenen Invarianten erzwingt. In C++ macht man genau das - und eine der Invarianten, die es bereitstellt, ist, dass bei Zerstörung des Objekts die von diesem Objekt kontrollierten Ressourcen (alle, nicht nur der Speicher) korrekt zerstört werden.

Java (und ähnliche) sind etwas anders. Während sie ein finalize unterstützen, das theoretisch ähnliche Funktionen bieten könnte, ist die Unterstützung so schwach, dass sie im Grunde unbrauchbar ist (und tatsächlich im Wesentlichen nie verwendet wird).

Infolgedessen muss der Client der Klasse Schritte unternehmen, anstatt dass die Klasse selbst die erforderliche Bereinigung durchführen kann. Wenn wir einen ausreichend kurzsichtigen Vergleich durchführen, kann es auf den ersten Blick so aussehen, als ob dieser Unterschied ziemlich gering ist und Java ist in dieser Hinsicht ziemlich wettbewerbsfähig mit C++. Wir haben am Ende so etwas. In C++ sieht die Klasse ungefähr so ​​aus:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

... und der Client-Code sieht ungefähr so ​​aus:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

In Java tauschen wir etwas mehr Code aus, wobei das Objekt für etwas weniger in der Klasse verwendet wird. Dies anfangs sieht nach einem ziemlich ausgeglichenen Kompromiss aus. In Wirklichkeit Es ist jedoch weit davon entfernt, da wir in den meisten typischen Codes die Klasse nur an einer Stelle definieren, aber wir verwenden an vielen Stellen. Der C++ - Ansatz bedeutet, dass wir nur diese schreiben Code, um die Bereinigung an einem Ort durchzuführen. Der Ansatz Java bedeutet, dass wir diesen Code schreiben müssen, um die Bereinigung an vielen Stellen um ein Vielfaches durchzuführen - an jedem Ort, an dem wir ein Objekt dieser Klasse verwenden .

Kurz gesagt, der Ansatz Java) garantiert grundsätzlich, dass viele Abstraktionen, die wir bereitstellen möchten, "undicht" sind - jede Klasse, die eine deterministische Bereinigung erfordert, verpflichtet den Client der Klasse, die Details von zu kennen Was bereinigt werden soll und wie bereinigt werden soll, anstatt dass diese Details in der Klasse selbst verborgen bleiben.

Obwohl ich es oben "den Java Ansatz" genannt habe), sind try/finally und ähnliche Mechanismen unter anderen Namen nicht vollständig auf Java beschränkt Ein prominentes Beispiel: Die meisten (alle?) der .NET-Sprachen (z. B. C #) bieten dasselbe.

Neuere Iterationen von beiden Java und C #) bieten auch einen halben Punkt zwischen "classic" Java und C++ in dieser Hinsicht. In C # ein Objekt, das will Um die Bereinigung zu automatisieren, kann die IDisposable -Schnittstelle implementiert werden, die eine Dispose -Methode bereitstellt, die (zumindest vage) einem C++ - Destruktor ähnelt. Während diese kann über verwendet werden a try/finally Wie in Java automatisiert C # die Aufgabe a little more mit einer using -Anweisung, mit der Sie Ressourcen definieren können, die erstellt werden sollen Wenn ein Bereich eingegeben und zerstört wird, wenn der Bereich beendet wird. Obwohl der von C++ bereitgestellte Automatisierungs- und Sicherheitsgrad immer noch weit entfernt ist, stellt dies eine wesentliche Verbesserung gegenüber Java dar. Insbesondere kann der Klassenentwickler die Details von - zentralisieren wie, um die Klasse in ihrer Implementierung von IDisposable zu entsorgen. Alles, was dem Client-Programmierer übrig bleibt, ist die geringere Belastung, eine using -Anweisung zu schreiben, um sicherzustellen, dass die IDisposable Schnittstelle wird verwendet, wenn es angezeigt wird ld sein. In Java 7 und neuer) wurden die Namen geändert, um die Schuldigen zu schützen, aber die Grundidee ist im Grunde identisch.

15
Jerry Coffin

Ich kann nicht glauben, dass niemand anderes dies angesprochen hat (kein Wortspiel beabsichtigt) - Sie brauchen keine catch -Klausel!

Das ist völlig vernünftig:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

Keine catch -Klausel ist irgendwo zu sehen, da diese Methode mit diesen Ausnahmen nichts Nützliches tun kann; Sie können den Aufrufstapel an einen Handler weitergeben, der kann. Das Abfangen und erneute Auslösen von Ausnahmen in jeder Methode ist eine schlechte Idee, insbesondere wenn Sie nur dieselbe Ausnahme erneut auslösen. Es widerspricht völlig der Frage, wie die strukturierte Ausnahmebehandlung funktionieren soll (und ist hübsch -) kurz davor, von jeder Methode einen "Fehlercode" zurückzugeben, nur in der "Form" einer Ausnahme).

Was diese Methode tut , muss sie jedoch tun, um nach sich selbst aufzuräumen, damit die "Außenwelt" nie etwas über das Chaos wissen muss dass es sich hinein bekam. Die finally-Klausel macht genau das - unabhängig davon, wie sich die aufgerufenen Methoden verhalten, wird die finally-Klausel "auf dem Weg nach draußen" aus der Methode ausgeführt (und das Gleiche gilt für alle finally-Klausel zwischen dem Punkt, an dem die Ausnahme ausgelöst wird, und der eventuellen catch-Klausel, die sie behandelt); Jeder wird ausgeführt, während sich der Aufrufstapel "abwickelt".

13
Phill W.

Was würde passieren, wenn eine Ausnahme ausgelöst würde, die Sie nicht erwartet hatten? Der Versuch würde in der Mitte beendet und es wird keine catch-Klausel ausgeführt.

Der letzte Block besteht darin, dabei zu helfen und sicherzustellen, dass die Bereinigung unabhängig von der Ausnahme erfolgt.

9
ratchet freak

Einige Sprachen bieten sowohl Konstruktoren als auch Destruktoren für ihre Objekte an (z. B. C++, glaube ich). Mit diesen Sprachen können Sie das meiste (wohl alles) tun, was normalerweise in finally in einem Destruktor ausgeführt wird. Daher kann in diesen Sprachen eine finally -Klausel überflüssig sein.

In einer Sprache ohne Destruktoren (z. B. Java) ist es schwierig (möglicherweise sogar unmöglich), eine korrekte Bereinigung ohne die Klausel finally zu erreichen. NB - In Java gibt es eine finalise Methode, aber es gibt keine Garantie, dass sie jemals aufgerufen wird.

7
OldCurmudgeon

Versuchen Sie es schließlich und versuchen Sie zu fangen sind zwei verschiedene Dinge, die nur das Schlüsselwort "try" teilen. Persönlich hätte ich das gerne anders gesehen. Der Grund, warum Sie sie zusammen sehen, ist, dass Ausnahmen einen "Sprung" erzeugen.

Und try finally ist so konzipiert, dass Code ausgeführt wird, selbst wenn der Programmierfluss herausspringt. Sei es aus einer Ausnahme oder aus einem anderen Grund. Es ist eine gute Möglichkeit, eine Ressource zu erwerben und sicherzustellen, dass sie danach bereinigt wird, ohne sich um Sprünge sorgen zu müssen.

1
Pieter B

Da diese Frage C++ nicht als Sprache angibt, werde ich eine Mischung aus C++ und Java in Betracht ziehen, da sie einen anderen Ansatz zur Objektzerstörung verfolgen, der als eine der Alternativen vorgeschlagen wird.

Gründe, warum Sie möglicherweise einen finally-Block anstelle eines Codes nach dem try-catch-Block verwenden

  • sie kehren früh vom try-Block zurück: Beachten Sie dies

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    im Vergleich zu:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • sie kehren früh von den Fangblöcken zurück: Vergleichen

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • Sie werfen Ausnahmen neu. Vergleichen Sie:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

Diese Beispiele lassen es nicht schlecht erscheinen, aber oft interagieren mehrere dieser Fälle und es sind mehr als eine Ausnahme/ein Ressourcentyp im Spiel. finally kann dazu beitragen, dass Ihr Code nicht zu einem Alptraum für die Wartung wird.

Jetzt in C++ können diese mit bereichsbereichsbasierten Objekten behandelt werden. IMO hat dieser Ansatz jedoch zwei Nachteile: 1. Die Syntax ist weniger benutzerfreundlich. 2. Die Reihenfolge der Konstruktion, die die umgekehrte Reihenfolge der Zerstörung ist, kann die Dinge weniger klar machen.

In Java) können Sie die Finalize-Methode nicht einbinden, um Ihre Bereinigung durchzuführen, da Sie nicht wissen, wann dies geschehen wird Viel Spielraum bei der Entscheidung, wann es Dinge zerstört - oft nicht, wenn Sie es erwarten - entweder früher oder später als erwartet - und das kann sich ändern, wenn der Hot-Spot-Compiler einschaltet ... seufz ...)

1

Alles, was in einer Programmiersprache logisch "notwendig" ist, sind die Anweisungen:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

Jeder Algorithmus kann nur mit den obigen Anweisungen implementiert werden. Alle anderen Sprachkonstrukte dienen dazu, Programme einfacher zu schreiben und für andere Programmierer verständlicher zu machen.

Siehe oldie worldy computer für die tatsächliche Hardware unter Verwendung eines solchen minimalen Befehlssatzes.

1
James Anderson

Tatsächlich liegt die größere Lücke für mich normalerweise in den Sprachen, die finally unterstützen, aber keine Destruktoren haben, weil Sie alle die mit "Bereinigung" verbundene Logik modellieren können (die ich in zwei Teile aufteilen werde) Kategorien) durch Destruktoren auf zentraler Ebene, ohne die Bereinigungslogik in jeder relevanten Funktion manuell zu behandeln. Wenn ich C # oder Java Code sehe, der Dinge wie das manuelle Entsperren von Mutexen und das Schließen von Dateien in finally Blöcken ausführt, fühlt sich das veraltet an und ähnelt C-Code, wenn all dies in C++ automatisiert ist durch Zerstörer auf eine Weise, die den Menschen von dieser Verantwortung befreit.

Ich würde jedoch immer noch eine leichte Bequemlichkeit finden, wenn C++ finally enthalten würde, und das liegt daran, dass es zwei Arten von Bereinigungen gibt:

  1. Lokale Ressourcen zerstören/freigeben/entsperren/schließen/etc (Destruktoren sind dafür perfekt).
  2. Rückgängigmachen/Zurücksetzen externer Nebenwirkungen (Destruktoren sind hierfür ausreichend).

Der zweite ist zumindest nicht so intuitiv auf die Idee der Ressourcenzerstörung abgestimmt, obwohl Sie dies mit Scope Guards tun können, die Änderungen automatisch rückgängig machen, wenn sie vor dem Festschreiben zerstört werden. Dort bietet finally wohl mindestens einen geringfügig (nur um ein kleines bisschen) einfacheren Mechanismus für den Job als Scope Guards.

Ein noch einfacherer Mechanismus wäre jedoch ein rollback Block, den ich noch nie in einer Sprache gesehen habe. Es ist eine Art Wunschtraum von mir, wenn ich jemals eine Sprache entworfen habe, die die Behandlung von Ausnahmen beinhaltet. Es würde so aussehen:

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

Dies wäre der einfachste Weg, um Rollbacks für Nebenwirkungen zu modellieren, während Destruktoren so ziemlich der perfekte Mechanismus für die Bereinigung lokaler Ressourcen sind. Jetzt werden nur noch ein paar zusätzliche Codezeilen aus der Scope Guard-Lösung gespeichert, aber der Grund, warum ich eine Sprache mit dieser Sprache sehen möchte, ist, dass das Rollback von Nebenwirkungen der am meisten vernachlässigte (aber schwierigste) Aspekt der Ausnahmebehandlung ist in Sprachen, die sich um Veränderlichkeit drehen. Ich denke, diese Funktion würde Entwickler dazu ermutigen, über die richtige Behandlung von Ausnahmen nachzudenken, wenn Transaktionen zurückgesetzt werden sollen, wenn Funktionen Nebenwirkungen verursachen und nicht abgeschlossen werden können. Als Nebenbonus, wenn die Leute sehen, wie schwierig es sein kann, Rollbacks ordnungsgemäß durchzuführen, Sie könnten es vorziehen, mehr Funktionen ohne Nebenwirkungen zu schreiben.

Es gibt auch einige dunkle Fälle, in denen Sie nur verschiedene Dinge tun möchten, unabhängig davon, was beim Beenden einer Funktion passiert, unabhängig davon, wie sie beendet wurde, z. B. das Protokollieren eines Zeitstempels. Dort ist finally wohl die einfachste und perfekteste Lösung für den Job, da der Versuch, ein Objekt nur zu instanziieren, um seinen Destruktor nur zum Protokollieren eines Zeitstempels zu verwenden, sich wirklich komisch anfühlt (obwohl Sie es einfach tun können gut und ganz bequem mit Lambdas).

0
user204677