Einige Sprachen (wie C ++ und frühe Versionen von PHP) unterstützen den finally
Teil eines try ... catch ... finally
Konstrukts nicht. Ist finally
jemals notwendig? Warum sollte ich diesen Code nicht einfach nach einem try ... catch
Block ohne finally
Klausel 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.)
exception-handling
Agi Hammerthief
quelle
quelle
Antworten:
Zusätzlich zu dem, was andere gesagt haben, ist es auch möglich, dass eine Ausnahme in die catch-Klausel aufgenommen wird. Bedenken Sie:
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.quelle
Wie andere bereits erwähnt haben, 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:kann neu geschrieben werden 1 als:
Letzteres erfordert jedoch, dass Sie alle nicht behandelten Ausnahmen abfangen, den Bereinigungscode duplizieren und daran denken, erneut zu werfen. Also
finally
nicht nötig , aber nützlich .C ++ hat keine,
finally
weil Bjarne Stroustrup glaubt, dass RAII besser ist , oder zumindest für die meisten Fälle ausreicht: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;
.quelle
handleError()
zweiten Fall fangen , nicht wahr?catch (Throwable t) {}
, mit dem try .. catch-Block um den gesamten Anfangsblock (umhandleError
auchhandleErro();
wodurch das Argument, warum Blöcke letztendlich nützlich sind, noch besser wird (obwohl dies nicht die ursprüngliche Frage war).finally
, was viel nuancierter ist.try
befindet sich in dercatch
fü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,cleanUp
Läufe zu garantieren, darin besteht, alles abzufangen , aber der ursprüngliche Code würde zulassen, dass sich Ausnahmen, die aus demcatch (SpecificException e)
Block stammen, nach oben ausbreiten.finally
Blöcke werden normalerweise zum Löschen von Ressourcen verwendet, um die Lesbarkeit bei Verwendung mehrerer return-Anweisungen zu verbessern:vs
quelle
finally
. (Ich würde Code wie im zweiten Block verwenden, da mehrere return-Anweisungen nicht empfohlen werden, wenn ich arbeite.)Wie Sie anscheinend bereits vermutet haben, bietet C ++ ohne diesen Mechanismus dieselben Funktionen. Als solches ist der
try
/finally
Mechanismus 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
/finally
bietet 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
finalize
bieten, 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:
... und der Client-Code sieht ungefähr so aus:
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
/finally
und ä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
IDisposable
Schnittstelle implementieren , die eineDispose
Methode 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 , wietry
finally
using
die Klasse in ihrer Umsetzung zu veräußernIDisposable
. Alles, was dem Client-Programmierer bleibt, ist die geringere Belastung durch das Schreiben einerusing
Anweisung, um sicherzustellen, dass dieIDisposable
Schnittstelle 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.quelle
Kann nicht glauben , niemand sonst diese erhoben hat (kein Wortspiel beabsichtigt) - Sie müssen nicht brauchen eine Fangklausel!
Das ist völlig vernünftig:
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".
quelle
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.
quelle
finally
, da Sie "unerwartete" Ausnahmen mitcatch(Object)
odercatch(...)
Allrounder verhindern können.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
finally
in einem Destruktor gemacht wird.finally
Insofern 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
finally
Klausel eine korrekte Bereinigung zu erreichen . NB - In Java gibt es einefinalise
Methode, aber es gibt keine Garantie, dass sie jemals aufgerufen wird.quelle
finalise
aber ich würde es vorziehen, zu diesem Zeitpunkt nicht auf die politischen Auseinandersetzungen um Destruktoren / Finales einzugehen.finalise
aber mit einem erweiterbaren Geschmack und einem oop-ähnlichen Mechanismus - sehr ausdrucksstark und vergleichbar mit demfinalise
Mechanismus anderer Sprachen.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.
quelle
try catch
aber nicht unterstützttry finally
. Code, der den letzteren verwendet, wird nur unter Verwendung des ersteren in Code konvertiert, indem der Inhalt desfinally
Blocks an allen Stellen des Codes kopiert wird, an denen er möglicherweise ausgeführt werden muss.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
im Vergleich zu:
Sie kehren früh von den Fangblöcken zurück: Vergleichen Sie
vs:
Sie werfen Ausnahmen zurück. Vergleichen Sie:
vs:
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.
finally
kann 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 ...)
quelle
Alles, was in einer Programmiersprache logischerweise "notwendig" ist, sind die Anweisungen:
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 .
quelle
Tatsächlich liegt die größere Lücke für mich normalerweise in den Sprachen, die
finally
Destruktoren 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 infinally
Blö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
finally
und es zwei Arten von Bereinigungen gibt: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
finally
wohl 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
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 Ausnahmebehandlung beinhaltet. Es würde ungefähr so aussehen: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
finally
gibt 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
Wie so viele andere ungewöhnliche Dinge in der C ++ - Sprache ist das Fehlen eines
try/finally
Konstrukts 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
finally
kann 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: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/finally
Konstrukt ist, das C ++ bietet, haben C ++ - Entwickler eine ziemlich kurze Sichttry/finally
: Wenn Sie nur einen Hammer haben, sieht alles sozusagen wie ein Destruktor aus.Ein erfahrener Entwickler kann aber auch andere Dinge mit einem
finally
Konstrukt 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:
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
DatasetEnabler
oder so heißen. Seine gesamte Existenz wäre als RAII-Helfer. Dann müssten Sie so etwas tun: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/finally
Block 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.
quelle