Wird Try / finally (ohne den Fang) die Ausnahme darstellen?

115

Ich bin fast sicher, dass die Antwort JA ist. Wenn ich einen Try finally-Block verwende, aber keinen Catch-Block verwende, sprudeln alle Ausnahmen. Richtig?

Irgendwelche Gedanken zur Praxis im Allgemeinen?

Seth

Seth Spearman
quelle

Antworten:

130

Ja, das wird es absolut. Angenommen, Ihr finallyBlock löst natürlich keine Ausnahme aus. In diesem Fall "ersetzt" dies effektiv die ursprünglich ausgelöste.

Jon Skeet
quelle
13
@ David: Sie können nicht von einem finally-Block in C # zurückkehren.
Jon Skeet
3
Die msdn-Dokumentation bestätigt auch diese Antwort: Alternativ können Sie die Ausnahme abfangen, die möglicherweise im try-Block einer try-finally-Anweisung weiter oben im Aufrufstapel ausgelöst wird. Das heißt, Sie können die Ausnahme in der Methode abfangen, die die Methode aufruft, die die try-finally-Anweisung enthält, oder in der Methode, die diese Methode aufruft, oder in einer beliebigen Methode im Aufrufstapel. Wenn die Ausnahme nicht abgefangen wird, hängt die Ausführung des finally-Blocks davon ab, ob das Betriebssystem eine Ausnahme-Abwicklungsoperation auslöst.
Breitband
60

Irgendwelche Gedanken zur Praxis im Allgemeinen?

Ja. Sei vorsichtig . Wenn Ihr finally-Block ausgeführt wird, ist es durchaus möglich, dass er ausgeführt wird, da eine nicht behandelte, unerwartete Ausnahme ausgelöst wurde . Das bedeutet, dass etwas kaputt ist und etwas völlig Unerwartetes passieren kann.

In dieser Situation ist es wohl der Fall, dass Sie Code überhaupt nicht in endgültigen Blöcken ausführen sollten. Der Code im finally-Block kann so erstellt werden, dass angenommen wird, dass Subsysteme, von denen er abhängt, fehlerfrei sind, obwohl sie tatsächlich stark beschädigt werden können. Der Code im finally-Block könnte die Situation verschlimmern.

Zum Beispiel sehe ich oft so etwas:

DisableAccessToTheResource();
try
{
    DoSomethingToTheResource();
}
finally
{
    EnableAccessToTheResource();
}

Der Autor dieses Codes denkt: "Ich mache eine vorübergehende Mutation zum Zustand der Welt. Ich muss den Zustand wieder so machen, wie er war, bevor ich gerufen wurde." Aber lassen Sie uns über alle Möglichkeiten nachdenken, wie dies schief gehen könnte.

Erstens könnte der Anrufer den Zugriff auf die Ressource bereits deaktivieren. In diesem Fall wird es durch diesen Code möglicherweise vorzeitig wieder aktiviert.

Zweitens, wenn DoSomethingToTheResource eine Ausnahme auslöst, ist es richtig, den Zugriff auf die Ressource zu ermöglichen? Der Code, der die Ressource verwaltet, ist unerwartet fehlerhaft . In diesem Code heißt es tatsächlich: "Wenn der Verwaltungscode fehlerhaft ist, stellen Sie sicher, dass anderer Code diesen fehlerhaften Code so schnell wie möglich aufrufen kann, damit er auch schrecklich fehlschlägt ." Das scheint eine schlechte Idee zu sein.

Drittens, wenn DoSomethingToTheResource eine Ausnahme auslöst, woher wissen wir dann, dass EnableAccessToTheResource nicht auch eine Ausnahme auslöst? Welche Schrecklichkeit sich auch auf die Verwendung der Ressource auswirkt, kann sich auch auf den Bereinigungscode auswirken. In diesem Fall geht die ursprüngliche Ausnahme verloren und das Problem ist schwerer zu diagnostizieren.

Ich neige dazu, Code wie diesen zu schreiben, ohne try-finally-Blöcke zu verwenden:

bool wasDisabled = IsAccessDisabled();
if (!wasDisabled)
    DisableAccessToTheResource();
DoSomethingToTheResource();
if (!wasDisabled)
    EnableAccessToTheResource();

Jetzt ist der Staat nicht mutiert, es sei denn, es muss sein. Jetzt wird der Status des Anrufers nicht durcheinander gebracht. Und jetzt, wenn DoSomethingToTheResource fehlschlägt, aktivieren wir den Zugriff nicht erneut. Wir gehen davon aus, dass etwas zutiefst kaputt ist, und riskieren nicht, die Situation zu verschlimmern, indem wir versuchen, weiterhin Code auszuführen. Lassen Sie den Anrufer das Problem lösen, wenn er kann.

Wann ist es also eine gute Idee, einen finally-Block auszuführen? Erstens, wenn die Ausnahme erwartet wird. Beispielsweise können Sie erwarten, dass ein Versuch, eine Datei zu sperren, fehlschlägt, weil jemand anderes sie gesperrt hat. In diesem Fall ist es sinnvoll, die Ausnahme abzufangen und dem Benutzer zu melden. In diesem Fall wird die Unsicherheit darüber, was kaputt ist, verringert; Es ist unwahrscheinlich, dass Sie die Situation durch Aufräumen verschlimmern.

Zweitens, wenn die Ressource, die Sie bereinigen, eine knappe Systemressource ist. Beispielsweise ist es sinnvoll, ein Dateihandle in einem finally-Block zu schließen. (Eine "Verwendung" ist natürlich nur eine andere Möglichkeit, einen Try-finally-Block zu schreiben.) Der Inhalt der Datei ist möglicherweise beschädigt, aber Sie können jetzt nichts dagegen tun. Das Dateihandle wird irgendwann geschlossen, also kann es auch eher früher als später sein.

Eric Lippert
quelle
7
Noch mehr Beispiele dafür, wie wir uns als Branche in Bezug auf die Fehlerbehandlung noch nicht wirklich zusammengetan haben. Nicht, dass ich etwas Besseres als Ausnahmen vorschlagen könnte, aber ich hoffe, dass die Zukunft etwas mit größerer Wahrscheinlichkeit dazu führt, dass die richtigen Maßnahmen relativ einfach ergriffen werden.
Jon Skeet
In vb.net kann Code in einem finally-Block erkennen, welche Ausnahme aussteht. Wenn man eine Ausnahmehierarchie einrichtet, um zu unterscheiden, dass "nicht getan hat; Zustand sonst in Ordnung" von "Zustand ist unterbrochen", kann ein Block "finally" nur ausgeführt werden, wenn die ausstehende Ausnahme keine der schlechten ist. Ich möchte, dass Disposer- / Bereinigungsroutinen Ausnahmen abfangen und eine DisposerFailedException auslösen (die in der Kategorie "schlecht" liegt und die ursprüngliche Ausnahme als InnerException enthält). Ich würde gerne eine Standard-iDisposableEx-Schnittstelle mit einer Dispose (Ex als Ausnahme) sehen, um dies zu erleichtern.
Supercat
Obwohl ich zu schätzen weiß, dass es Fälle gibt, in denen eine Ausnahme auftreten kann, während sich ein Objekt in einem schlechten Zustand befindet und eine versuchte Bereinigung die Situation verschlimmert, denke ich, dass solche Probleme im Bereinigungscode behandelt werden sollten, möglicherweise mithilfe von Delegierten. Ein Sperr-Wrapper kann beispielsweise einen Funktionsdelegaten "CheckIfSafeToUnlock (Ex as Exception)" verfügbar machen, der zu Zeiten festgelegt werden kann, in denen sich das gesperrte Objekt in einem ungültigen Zustand befindet und anderweitig gelöscht wird. Vor dem Aufheben der Sperre konnte der Wrapper den Delegaten überprüfen. Wenn es nicht leer ist, kann es ausgeführt und die Sperre nur aufgehoben werden, wenn true zurückgegeben wird.
Supercat
Wenn ein Wrapper einen solchen Delegaten verwendet, kann der Code, der den Objektstatus manipuliert hat, eine geeignete Bereinigungsaktion auswählen, wenn er unterbrochen wird. Solche Aktionen können das Reparieren der Datenstruktur und das Zurückgeben von True, das Auslösen einer weiteren "schwerwiegenderen" Ausnahme, die die ursprüngliche Ausnahme umschließt, oder das Versickernlassen der ursprünglichen Ausnahme bei der Rückgabe von False umfassen.
Supercat
2
Eric, danke für deine tolle Antwort. Ich denke, ich hätte zwei Fragen stellen sollen, weil sowohl Sie als auch Jon Recht haben. In jedem Fall sind Sie positiv bewertet. Vielen Dank für Ihre Aufmerksamkeit auf die Frage.
Seth Spearman