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

25

Einige Sprachen (wie C ++ und frühe Versionen von PHP) unterstützen den finallyTeil eines try ... catch ... finallyKonstrukts nicht. Ist finallyjemals notwendig? Warum sollte ich diesen Code nicht einfach nach einem try ... catchBlock ohne finallyKlausel einfügen, weil der Code darin immer läuft ? Warum eins benutzen? (Ich suche einen Grund / eine Motivation für das Verwenden / Nicht-Verwenden finally, keinen Grund, das "Fangen" aufzuheben oder warum es legal ist, dies zu tun.)

Agi Hammerthief
quelle
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
maple_shaft

Antworten:

36

Zusätzlich zu dem, was andere gesagt haben, ist es auch möglich, dass eine Ausnahme in die catch-Klausel aufgenommen wird. Bedenken Sie:

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

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

Erik
quelle
4
Vielen Dank für eine prägnante und direkte Antwort, die nicht in die Theorie eintaucht und "Sprache X ist besser als Y".
Agi Hammerthief
56

Wie andere bereits erwähnt haben, gibt es keine Garantie dafür, dass Code nach einer tryAnweisung 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 neu geschrieben werden 1 als:

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. Also finallynicht nötig , aber nützlich .

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

Warum liefert 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, mit dem alle Ausnahmen abgefangen und erneut geworfen werden können, ohne dass Stack-Trace-Informationen verloren gehen, variiert je nach Sprache. Ich habe Java verwendet, bei dem der Stack-Trace erfasst wird, wenn die Ausnahme erstellt wird. In C # würden Sie nur verwenden throw;.

Doval
quelle
8
Sie müssen auch die Ausnahmen im handleError()zweiten Fall fangen , nicht wahr?
Juri Robl
1
Sie können auch einen Fehler auslösen. Ich würde das umformulieren catch (Throwable t) {}, mit dem try .. catch-Block um den gesamten Anfangsblock (um handleErrorauch
Wurfobjekte
1
Tatsächlich würde ich den zusätzlichen Try-Catch hinzufügen, den Sie beim Aufrufen weggelassen haben, handleErro();wodurch das Argument, warum Blöcke letztendlich nützlich sind, noch besser wird (obwohl dies nicht die ursprüngliche Frage war).
Alex
1
Diese Antwort geht nicht wirklich auf die Frage ein, warum C ++ keine hat finally, was viel nuancierter ist.
DeadMG
1
@AgiHammerthief Das verschachtelte Objekt trybefindet sich in der catchfür die bestimmte Ausnahme. Zweitens ist es möglich, dass Sie nicht wissen, ob Sie den Fehler erfolgreich behandeln können, bis Sie die Ausnahme untersucht haben, oder dass die Ursache der Ausnahme Sie auch daran hindert, den Fehler zu behandeln (zumindest auf dieser Ebene). Dies ist ziemlich häufig bei E / A-Vorgängen. Der Rethrow ist da, weil die einzige Möglichkeit, cleanUpLäufe zu garantieren, darin besteht, alles abzufangen , aber der ursprüngliche Code würde zulassen, dass sich Ausnahmen, die aus dem catch (SpecificException e)Block stammen, nach oben ausbreiten.
Doval
22

finally Blöcke werden normalerweise zum Löschen von Ressourcen verwendet, um die Lesbarkeit bei Verwendung mehrerer return-Anweisungen zu verbessern:

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;
}
AlexFoxGill
quelle
2
Ich denke das ist die beste Antwort. Ein Endlich als Ersatz für eine generische Ausnahme zu verwenden, scheint einfach beschissen. Der richtige Anwendungsfall besteht darin, Ressourcen oder ähnliche Vorgänge zu bereinigen.
Kik
3
Vielleicht ist es sogar noch üblicher, innerhalb des try-Blocks zurückzukehren, als innerhalb des catch-Blocks.
Michael Anderson
Meiner Meinung nach erklärt der Code die Verwendung von nicht ausreichend finally. (Ich würde Code wie im zweiten Block verwenden, da mehrere return-Anweisungen nicht empfohlen werden, wenn ich arbeite.)
Agi Hammerthief
15

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

Das heißt, wenn Sie darauf verzichten, werden einige Anforderungen an die Gestaltung der restlichen Sprache gestellt. In C ++ ist dieselbe Menge von Aktionen in einem Klassendestruktor enthalten. Dies funktioniert hauptsächlich (ausschließlich?), Weil der Destruktoraufruf in C ++ deterministisch ist. Dies führt wiederum zu einigen recht komplexen Regeln für die Lebensdauer von Objekten, von denen einige entschieden nicht intuitiv sind.

Die meisten anderen Sprachen bieten stattdessen eine Art Garbage Collection an. Zwar gibt es umstrittene Aspekte der Speicherbereinigung (z. B. die Effizienz im Vergleich zu anderen Methoden der Speicherverwaltung), doch im Allgemeinen ist es nicht so, dass der genaue Zeitpunkt, zu dem ein Objekt vom Speicherbereiniger "bereinigt" wird, nicht direkt festgelegt wird auf den 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 es sich um Ressourcen handelt, die so kostbar sind, dass ihre Bereinigung nicht willkürlich verzögert wird. try/ finallybietet eine Möglichkeit für solche Sprachen, mit Situationen umzugehen, die eine deterministische Bereinigung erfordern.

Ich denke, diejenigen, die behaupten, die C ++ - Syntax für diese Funktion sei "weniger benutzerfreundlich" als die von Java, verpassen eher den Punkt. Schlimmer noch, es fehlt ein viel entscheidenderer Punkt in Bezug auf die Aufteilung der Verantwortung, der weit über die Syntax hinausgeht und wesentlich 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 Entwurfs - 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, für die es sorgt, ist, dass die von diesem Objekt gesteuerten Ressourcen (alle, nicht nur der Speicher) korrekt zerstört werden, wenn das Objekt zerstört wird.

Java (und ähnliche) sind etwas anders. Während sie eine Unterstützung finalizebieten, die theoretisch ähnliche Fähigkeiten bieten könnte, ist die Unterstützung so schwach, dass sie im Grunde unbrauchbar ist (und im Grunde genommen nie verwendet wird).

Infolgedessen muss der Client der Klasse Schritte ausführen, damit die Klasse selbst die erforderliche Bereinigung durchführen kann . Wenn wir einen ausreichend kurzsichtigen Vergleich anstellen, kann auf den ersten Blick der Eindruck entstehen, dass dieser Unterschied relativ gering ist und Java in dieser Hinsicht mit C ++ durchaus konkurrenzfähig ist. Am Ende haben wir 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 Code in der Klasse verwendet wird. Dies sieht zunächst nach einem ziemlich ausgeglichenen Kompromiss aus. In Wirklichkeit ist es weit davon entfernt aber, weil in den meisten typischen Code nur wir die Klasse in definieren einen Ort, aber wir verwenden es viele Orte. Der C ++ - Ansatz bedeutet, dass wir diesen Code nur schreiben, um die Bereinigung an einem Ort durchzuführen. Der Java-Ansatz bedeutet, dass wir diesen Code schreiben müssen, um die Bereinigung an vielen Stellen zu erledigen - an jeder Stelle, an der wir ein Objekt dieser Klasse verwenden.

Kurz gesagt, der Java-Ansatz garantiert im Grunde genommen, dass viele Abstraktionen, die wir bereitstellen möchten, "undicht" sind - jede Klasse, die eine deterministische Bereinigung erfordert, verpflichtet den Client der Klasse, über die Details zu wissen, was bereinigt werden soll und wie die Bereinigung durchgeführt werden soll und nicht, dass diese Details in der Klasse selbst versteckt sind.

Obwohl ich es oben als "Java-Ansatz" bezeichnet habe, sind try/ finallyund ähnliche Mechanismen unter anderen Namen nicht vollständig auf Java beschränkt. Für ein prominentes Beispiel bieten die meisten (alle?) .NET-Sprachen (z. B. C #) dasselbe.

Jüngste Iterationen von Java und C # bieten in dieser Hinsicht auch eine Halbwertszeit zwischen "klassischem" Java und C ++. In C # kann ein Objekt, das seine Bereinigung automatisieren möchte, die IDisposableSchnittstelle implementieren , die eine DisposeMethode bereitstellt, die einem C ++ - Destruktor (zumindest vage) ähnlich ist. Während dies in Java über a / like verwendet werden kann , automatisiert C # die Aufgabe ein wenig mehr mit einer Anweisung, mit der Sie Ressourcen definieren können, die beim Eingeben eines Bereichs erstellt und beim Verlassen des Bereichs zerstört werden. Obwohl C ++ noch weit hinter dem Automatisierungsgrad und der Sicherheit zurückbleibt, ist dies eine erhebliche Verbesserung gegenüber Java. Insbesondere kann die Klasse Designer zentralisieren die Details , wietryfinallyusingdie Klasse in ihrer Umsetzung zu veräußern IDisposable. Alles, was dem Client-Programmierer bleibt, ist die geringere Belastung durch das Schreiben einer usingAnweisung, um sicherzustellen, dass die IDisposableSchnittstelle zum richtigen Zeitpunkt verwendet wird. In Java 7 und neuer wurden die Namen geändert, um die Schuldigen zu schützen, aber die Grundidee ist im Grunde identisch.

Jerry Sarg
quelle
1
Perfekte Antwort. Destruktoren sind DAS Muss in C ++.
Thomas Eding
13

Kann nicht glauben , niemand sonst diese erhoben hat (kein Wortspiel beabsichtigt) - Sie müssen nicht brauchen eine Fangklausel!

Das ist völlig vernünftig:

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

Kein Fang - Klausel überall ist Sicht, da diese Methode nicht alles tun kann nützlich mit diesen Ausnahmen; Sie müssen den Aufrufstapel an einen Handler weitergeben, der dies kann . Ausnahmen in jeder Methode abfangen und erneut auslösen ist eine schlechte Idee, besonders wenn Sie nur dieselbe Ausnahme erneut auslösen. Es geht völlig gegen wie Structured Exception Handling ist angeblich an der Arbeit (und ist ziemlich nah einen „Fehlercode“ von jeder Methode auf der Rückkehr, nur in der „Form“ eine Ausnahme).

Was diese Methode jedoch zu tun hat, ist, nach sich selbst aufzuräumen, damit die "Außenwelt" nie etwas über das Durcheinander wissen muss, in das sie sich selbst gebracht hat. Die finally-Klausel macht genau das - egal wie sich die aufgerufenen Methoden verhalten, die finally-Klausel wird "auf dem Ausweg" der Methode ausgeführt (und das Gleiche gilt für jede finally-Klausel zwischen dem Punkt, an dem die Exception ausgelöst wird, und die eventuelle catch-Klausel, die dies behandelt); Jeder wird ausgeführt, während sich der Aufrufstapel "abwickelt".

Phill W.
quelle
9

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

Der Block finally soll dabei helfen und sicherstellen, dass unabhängig von der Ausnahme die Bereinigung erfolgt.

Ratschenfreak
quelle
4
Das ist kein ausreichender Grund für eine finally, da Sie "unerwartete" Ausnahmen mit catch(Object)oder catch(...)Allrounder verhindern können.
MSalters
1
Das klingt nach einer Umgehung. Endlich ist Konzeptionell sauberer. Allerdings muss ich zugeben, dass ich es selten benutze.
quick_now
7

Einige Sprachen bieten sowohl Konstruktoren als auch Destruktoren für ihre Objekte an (z. B. C ++, glaube ich). Mit diesen Sprachen können Sie fast alles tun, was normalerweise finallyin einem Destruktor gemacht wird. finallyInsofern kann in diesen Sprachen eine Klausel überflüssig sein.

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

OldCurmudgeon
quelle
Es kann nützlich sein zu beachten, dass Destruktoren beim Reinigen von Ressourcen helfen, wenn die Zerstörung deterministisch ist . Wenn wir nicht wissen, wann das Objekt zerstört und / oder Müll gesammelt wird, sind Destruktoren nicht sicher genug.
Morwenn
@ Morwenn - Guter Punkt. Ich habe es mit meinem Verweis auf Java angedeutet, finaliseaber ich würde es vorziehen, zu diesem Zeitpunkt nicht auf die politischen Auseinandersetzungen um Destruktoren / Finales einzugehen.
OldCurmudgeon
In C ++ ist die Zerstörung deterministisch. Wenn der Gültigkeitsbereich, der ein automatisches Objekt enthält, beendet wird (z. B. wenn es vom Stapel abfällt), wird sein Destruktor aufgerufen. (Mit C ++ können Sie Objekte auf dem Stapel zuordnen, nicht nur den Haufen.)
Rob K
@RobK - Und das ist die genaue Funktionalität eines, finaliseaber mit einem erweiterbaren Geschmack und einem oop-ähnlichen Mechanismus - sehr ausdrucksstark und vergleichbar mit dem finaliseMechanismus anderer Sprachen.
OldCurmudgeon
1

Endlich probieren und versuchen, fangen sind zwei verschiedene Dinge, die nur das Schlüsselwort "try" gemeinsam haben. Persönlich hätte ich das gerne anders gesehen. Der Grund, warum Sie sie zusammen sehen, ist, dass Ausnahmen einen "Sprung" hervorrufen.

Und try finally wurde entwickelt, um Code auch dann auszuführen, wenn der Programmierfluss aus dem Ruder läuft. Sei es wegen einer Ausnahme oder aus irgendeinem anderen Grund. Es ist eine gute Möglichkeit, eine Ressource zu erwerben und sicherzustellen, dass sie nach dem Aufräumen beseitigt ist, ohne sich um Sprünge sorgen zu müssen.

Pieter B
quelle
3
In .NET werden sie mithilfe separater Mechanismen implementiert. In Java ist das einzige Konstrukt, das von der JVM erkannt wird, semantisch gleichbedeutend mit "on error goto", einem Muster, das direkt unterstützt, try catchaber nicht unterstützt try finally. Code, der den letzteren verwendet, wird nur unter Verwendung des ersteren in Code konvertiert, indem der Inhalt des finallyBlocks an allen Stellen des Codes kopiert wird, an denen er möglicherweise ausgeführt werden muss.
Supercat
@supercat nett, danke für die extra Info über Java.
Pieter B
1

Da diese Frage C ++ nicht als Sprache angibt, werde ich eine Mischung aus C ++ und Java betrachten, 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 von Code nach dem try-catch-Block verwenden

  • Sie kehren früh aus dem Try-Block zurück: Betrachten 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 Sie

    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 zurück. 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 so schlimm erscheinen, aber oft haben Sie mehrere dieser Fälle in Wechselwirkung und mehr als einen Ausnahme- / Ressourcentyp im Spiel. finallykann dazu beitragen, dass Ihr Code nicht zu einem Alptraum für die Wartung wird.

Jetzt in C ++ können diese mit bereichsbasierten Objekten behandelt werden. Aber IMO hat dieser Ansatz zwei Nachteile: 1. Die Syntax ist weniger benutzerfreundlich. 2. Die Reihenfolge des Aufbaus in umgekehrter Reihenfolge der Zerstörung 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 - (Sie können es, aber das ist ein Pfad voller unterhaltsamer Rennbedingungen - JVM hat viel Entscheidungsspielraum, wann es zerstört wird Dinge - oft ist es nicht so, wie Sie es erwarten - entweder früher oder später als erwartet - und das kann sich ändern, wenn der Hot-Spot-Compiler einsetzt ... seufz ...)

Michael Anderson
quelle
1

Alles, was in einer Programmiersprache logischerweise "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 wie einen minimalen Befehlssatz verwendet wird .

James Anderson
quelle
1
Ihre Antwort ist sicherlich richtig, aber ich codiere nicht in Assembler; es ist zu schmerzhaft. Ich frage, warum ich eine Funktion verwende, die in den Sprachen, die sie unterstützen, nicht den Sinn hat und nicht den Mindestanweisungssatz einer Sprache.
Agi Hammerthief
1
Der Punkt ist, dass jede Sprache, die nur diese 5 Operationen implementiert, jeden Algorithmus implementieren kann - wenn auch ziemlich umständlich. Die meisten Vers / Operatoren in Hochsprachen sind nicht "notwendig", wenn nur ein Algorithmus implementiert werden soll. Wenn das Ziel eine schnelle Entwicklung von lesbarem wartbarem Code ist, dann sind die meisten notwendig, aber "lesbar" und "wartbar" sind nicht messbar und äußerst subjektiv. Die netten Sprachentwickler haben viele Funktionen eingebaut: Wenn Sie für einige von ihnen keine Verwendung haben, verwenden Sie sie nicht.
James Anderson
0

Tatsächlich liegt die größere Lücke für mich normalerweise in den Sprachen, die finallyDestruktoren unterstützen, aber nicht, da Sie die gesamte mit "Bereinigen" verbundene Logik (die ich in zwei Kategorien einteilen werde) durch Destruktoren auf einer zentralen Ebene modellieren können, ohne sich manuell mit Bereinigen zu befassen Logik in jeder relevanten Funktion. Wenn ich C # - oder Java-Code sehe, wie er Mutexe manuell entsperrt und Dateien in finallyBlöcken schließt, fühlt sich das veraltet und irgendwie wie C-Code an, wenn all dies in C ++ durch Destruktoren auf eine Weise automatisiert wird, die den Menschen von dieser Verantwortung befreit.

Ich würde jedoch immer noch eine milde Annehmlichkeit finden, wenn C ++ enthalten wäre finallyund es zwei Arten von Bereinigungen gibt:

  1. Zerstören / Freigeben / Entsperren / Schließen / etc lokaler Ressourcen (Destruktoren sind dafür perfekt).
  2. Externe Nebenwirkungen rückgängig machen / rückgängig machen (Destruktoren sind dafür ausreichend).

Zumindest der zweite Ansatz lässt sich der Idee der Ressourcenzerstörung nicht so intuitiv zuordnen. Sie können dies jedoch problemlos mit Bereichsschutzeinrichtungen tun, die Änderungen automatisch rückgängig machen, wenn sie vor dem Festschreiben zerstört werden. Es gibt finallywohl zumindest einen geringfügig (nur um ein kleines bisschen) einfacheren Mechanismus für den Job als Zielfernrohrschützer.

Ein noch unkomplizierterer Mechanismus wäre jedoch ein rollbackBlock, den ich noch nie in einer Sprache gesehen habe. Es ist eine Art Wunschtraum von mir, wenn ich jemals eine Sprache entworfen habe, die Ausnahmebehandlung beinhaltet. Es würde ungefähr 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 Nebeneffekt-Rollbacks zu modellieren, während Destruktoren praktisch der perfekte Mechanismus für die Bereinigung lokaler Ressourcen sind. Jetzt werden nur ein paar zusätzliche Codezeilen aus der Scope-Guard-Lösung gespart, aber der Grund, warum ich eine Sprache mit dieser darin sehen möchte, ist, dass das Nebeneffekt-Rollback der am meisten vernachlässigte (aber schwierigste) Aspekt der Ausnahmebehandlung ist in Sprachen, die sich um die Veränderbarkeit drehen. Ich denke, diese Funktion würde Entwickler dazu ermutigen, über die ordnungsgemäße Behandlung von Ausnahmen nachzudenken, um Transaktionen rückgängig zu machen, wenn Funktionen Nebenwirkungen verursachen und nicht abgeschlossen werden. Als Nebeneffekt, wenn die Leute erkennen, wie schwierig es sein kann, Rollbacks ordnungsgemäß durchzuführen. Sie könnten es vorziehen, mehr Funktionen zu schreiben, die frei von Nebenwirkungen sind.

Es gibt auch einige unklare Fälle, in denen Sie einfach verschiedene Dinge tun möchten, unabhängig davon, wie eine Funktion beendet wurde, z. B. das Aufzeichnen eines Zeitstempels. Es finallygibt wohl die einfachste und perfekteste Lösung für diesen Job, da sich der Versuch, ein Objekt zu instanziieren, nur um seinen Destruktor zum Protokollieren eines Zeitstempels zu verwenden, wirklich seltsam anfühlt (auch wenn Sie dies mit Lambdas ganz einfach und bequem tun können) ).


quelle
-9

Wie so viele andere ungewöhnliche Dinge in der C ++ - Sprache ist das Fehlen eines try/finallyKonstrukts ein Konstruktionsfehler, wenn man es auch so nennen kann, in einer Sprache, die häufig überhaupt keine eigentlichen Konstruktionsarbeiten durchgeführt zu haben scheint .

RAII (die Verwendung des auf Gültigkeitsbereichen basierenden deterministischen Destruktoraufrufs für stapelbasierte Objekte zur Bereinigung) weist zwei schwerwiegende Mängel auf. Das erste ist, dass die Verwendung von stapelbasierten Objekten erforderlich ist. Dies ist ein Greuel, der gegen das Liskov-Substitutionsprinzip verstößt. Es gibt viele gute Gründe, warum keine andere OO-Sprache vor oder seit C ++ sie verwendet hat - innerhalb von epsilon; D zählt nicht, da es stark auf C ++ basiert und ohnehin keinen Marktanteil hat - und die damit verbundenen Probleme zu erklären, würde den Rahmen dieser Antwort sprengen.

Zweitens finallykann eine Obermenge der Objektzerstörung erreicht werden. Vieles, was mit RAII in C ++ gemacht wird, wird in der Delphi-Sprache beschrieben, die keine Garbage Collection hat, mit dem folgenden Muster:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

Dies ist das explizite RAII-Muster. Wenn Sie eine C ++ - Routine erstellen würden, die nur das Äquivalent zur ersten und dritten Zeile oben enthält, würde das, was der Compiler generieren würde, so aussehen, wie ich es in seiner Grundstruktur geschrieben habe. Und weil dies der einzige Zugriff auf das try/finallyKonstrukt ist, das C ++ bietet, haben C ++ - Entwickler eine ziemlich kurze Sicht try/finally: Wenn Sie nur einen Hammer haben, sieht alles sozusagen wie ein Destruktor aus.

Ein erfahrener Entwickler kann aber auch andere Dinge mit einem finallyKonstrukt tun . Es geht nicht um deterministische Zerstörung, selbst wenn eine Ausnahme gemacht wird. Es geht um die deterministische Codeausführung , selbst wenn eine Ausnahme ausgelöst wird.

In Delphi-Code wird möglicherweise Folgendes angezeigt: Ein Dataset-Objekt, an das Benutzersteuerelemente gebunden sind. Das Dataset enthält Daten aus einer externen Quelle, und die Steuerelemente geben den Status der Daten wieder. Wenn Sie eine Reihe von Daten in Ihr Dataset laden möchten, sollten Sie die Datenbindung vorübergehend deaktivieren, damit die Benutzeroberfläche nicht durch ungewöhnliche Vorgänge beeinträchtigt wird. Aktualisieren Sie sie immer wieder, wenn ein neuer Datensatz eingegeben wird , so würden Sie es so codieren:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

Es ist klar, dass hier kein Gegenstand zerstört wird und dass keiner benötigt wird. Der Code ist einfach, präzise, ​​explizit und effizient.

Wie würde dies in C ++ geschehen? Nun, zuerst müssten Sie eine ganze Klasse codieren . Es würde wahrscheinlich so DatasetEnableroder so heißen. Seine gesamte Existenz wäre als RAII-Helfer. Dann müssten Sie so etwas tun:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

Ja, diese anscheinend überflüssigen geschweiften Klammern sind erforderlich, um den ordnungsgemäßen Gültigkeitsbereich zu verwalten und sicherzustellen, dass der Datensatz sofort und nicht am Ende der Methode wieder aktiviert wird. Was Sie am Ende haben, braucht also nicht weniger Codezeilen (es sei denn, Sie verwenden ägyptische Klammern). Es muss ein überflüssiges Objekt erstellt werden, das Overhead hat. (Soll C ++ - Code nicht schnell sein?) Er ist nicht explizit, sondern beruht auf der Magie des Compilers. Der Code, der ausgeführt wird, wird in dieser Methode nirgendwo beschrieben, sondern befindet sich in einer ganz anderen Klasse, möglicherweise in einer ganz anderen Datei . Kurz gesagt, es ist keine bessere Lösung, als den try/finallyBlock selbst schreiben zu können.

Diese Art von Problem ist im Sprachdesign so verbreitet, dass es einen Namen dafür gibt: Abstraktionsinversion. Es tritt auf, wenn ein Konstrukt auf hoher Ebene auf einem Konstrukt auf niedriger Ebene erstellt wird und das Konstrukt auf niedriger Ebene dann in der Sprache nicht direkt unterstützt wird, so dass diejenigen, die es verwenden möchten, es im Sinne von erneut implementieren müssen Hochstufiges Konstrukt, häufig mit erheblichen Einbußen bei der Lesbarkeit und Effizienz des Codes.

Mason Wheeler
quelle
Kommentare sollen eine Frage und eine Antwort verdeutlichen oder verbessern. Wenn Sie eine Diskussion über diese Antwort haben möchten, gehen Sie bitte in den Chatraum. Vielen Dank.
maple_shaft