Ich könnte der Zorn Pythonistas aufrufen (weiß nicht, wie ich Python nicht viel verwenden) oder Programmierer aus anderen Sprachen mit dieser Antwort, aber meiner Meinung nach die meisten Funktionen sollten nicht einen haben catch
Block, ideal gesprochen. Um zu zeigen, warum, lassen Sie mich dies mit der manuellen Weitergabe von Fehlercodes vergleichen, wie ich sie bei der Arbeit mit Turbo C Ende der 80er und Anfang der 90er Jahre ausführen musste.
Nehmen wir also an, wir haben eine Funktion zum Laden eines Bildes oder dergleichen, wenn ein Benutzer eine zu ladende Bilddatei auswählt, und dies ist in C und Assembly geschrieben:
Ich habe einige Funktionen auf niedriger Ebene weggelassen, aber wir können sehen, dass ich verschiedene Kategorien von Funktionen identifiziert habe, die farbcodiert sind, basierend auf den Verantwortlichkeiten, die sie in Bezug auf die Fehlerbehandlung haben.
Fehlerursache und Wiederherstellung
Nun war es nie schwer, die Kategorien von Funktionen zu schreiben, die ich als "mögliche throw
Fehlerquellen " (die, die das sind) und die "Fehlerbehebungs- und Berichterstellungsfunktionen" (die, die das catch
sind) bezeichne.
Es war immer trivial, diese Funktionen korrekt zu schreiben, bevor die Ausnahmebehandlung verfügbar war, da eine Funktion, die auf einen externen Fehler stoßen kann, wie z. B. die Nichtzuweisung von Speicher, einfach einen NULL
oder 0
oder -1
oder einen globalen Fehlercode oder etwas in diesem Sinne zurückgeben kann. Und die Fehlerbehebung / -meldung war immer einfach, da Sie, nachdem Sie sich den Call-Stack bis zu einem Punkt durchgearbeitet haben, an dem es sinnvoll war, Fehler zu beheben und zu melden, einfach den Fehlercode und / oder die Meldung nehmen und dem Benutzer melden. Und natürlich ist eine Funktion am Blatt dieser Hierarchie, die niemals ausfallen kann, egal wie sie in Zukunft geändert wird ( Convert Pixel
), kinderleicht zu schreiben (zumindest in Bezug auf die Fehlerbehandlung).
Fehlerausbreitung
Die mühsamen Funktionen, die für menschliches Versagen anfällig sind, sind die Fehlerausbreitungsprogramme , die nicht direkt auf Fehler stoßen , sondern Funktionen aufrufen, die irgendwo in der Hierarchie versagen könnten. An diesem Punkt Allocate Scanline
könnte einen Ausfall von handhaben müssen malloc
und dann einen Fehler zurück nach unten zu Convert Scanlines
, dann Convert Scanlines
würde für diesen Fehler überprüfen müssen und es weitergeben zu Decompress Image
, dann Decompress Image->Parse Image
, und Parse Image->Load Image
, und Load Image
den Benutzer-End - Befehl , wo der Fehler schließlich gemeldet .
Hier machen viele Menschen Fehler, da nur ein Fehler-Propagator den Fehler nicht überprüft und weitergibt, damit die gesamte Funktionshierarchie zusammenbricht, wenn es darum geht, den Fehler richtig zu behandeln.
Wenn Fehlercodes von Funktionen zurückgegeben werden, verlieren wir in etwa 90% unserer Codebasis die Fähigkeit, bei Erfolg interessante Werte zurückzugeben, da so viele Funktionen ihren Rückgabewert für die Rückgabe eines Fehlercodes reservieren müssten Scheitern .
Reduzierung menschlicher Fehler: Globale Fehlercodes
Wie können wir also die Möglichkeit menschlicher Fehler reduzieren? Hier könnte ich sogar den Zorn einiger C-Programmierer hervorrufen, aber eine sofortige Verbesserung meiner Meinung nach ist die Verwendung globaler Fehlercodes wie OpenGL mit glGetError
. Dies gibt den Funktionen zumindest die Möglichkeit, im Erfolgsfall aussagekräftige Werte von Interesse zurückzugeben. Es gibt Möglichkeiten, diesen Thread sicher und effizient zu gestalten, wenn der Fehlercode in einem Thread lokalisiert ist.
Es gibt auch Fälle, in denen eine Funktion möglicherweise auf einen Fehler stößt, es jedoch relativ harmlos ist, etwas länger zu warten, bevor sie aufgrund der Entdeckung eines früheren Fehlers vorzeitig zurückkehrt. Dies ermöglicht, dass so etwas passiert, ohne dass 90% der Funktionsaufrufe, die in jeder einzelnen Funktion ausgeführt werden, auf Fehler überprüft werden müssen, sodass eine ordnungsgemäße Fehlerbehandlung möglich ist, ohne dass dies so genau ist.
Reduzierung menschlicher Fehler: Ausnahmebehandlung
Die obige Lösung erfordert jedoch immer noch so viele Funktionen, um den Steuerflussaspekt der manuellen Fehlerausbreitung zu behandeln, selbst wenn sie die Anzahl der Zeilen des manuellen if error happened, return error
Codetyps verringert haben könnte . Es würde es nicht vollständig beseitigen, da es immer noch häufig mindestens eine Stelle geben müsste, die nach einem Fehler sucht und für fast jede einzelne Fehlerausbreitungsfunktion zurückkehrt. Dies ist also der Zeitpunkt, an dem die Ausnahmebehandlung ins Spiel kommt, um den Tag zu retten (sorta).
Der Wert der Ausnahmebehandlung besteht hier jedoch darin, den Kontrollflussaspekt der manuellen Fehlerausbreitung nicht mehr zu berücksichtigen. Dies bedeutet, dass sein Wert an die Fähigkeit gebunden ist, zu vermeiden, dass Sie catch
in Ihrer gesamten Codebasis eine Bootsladung von Blöcken schreiben müssen . In der obigen Abbildung ist der einzige Ort, an dem ein catch
Block vorhanden sein muss, der Ort, an Load Image User Command
dem der Fehler gemeldet wird. Nichts anderes sollte idealerweise catch
irgendetwas zu tun haben, da es sonst langsam so mühsam und fehleranfällig wird wie die Fehlercode-Behandlung.
Wenn Sie mich also fragen, ob Sie eine Codebasis haben, die auf elegante Weise wirklich von der Ausnahmebehandlung profitiert, sollte sie die Mindestanzahl von catch
Blöcken haben (mit Minimum meine ich nicht Null, sondern eher eine für jeden einzelnen High-End-Code). Endbenutzeroperation, die fehlschlagen könnte, und möglicherweise noch weniger, wenn alle High-End-Benutzeroperationen über ein zentrales Befehlssystem aufgerufen werden).
Ressourcenbereinigung
Die Ausnahmebehandlung beseitigt jedoch nur die Notwendigkeit, die Steuerflussaspekte der Fehlerausbreitung nicht manuell auf Pfaden zu behandeln, die von normalen Ausführungsabläufen getrennt sind. Oft kann eine Funktion, die als Fehler-Propagator dient, auch dann noch Ressourcen beschaffen, wenn sie dies jetzt automatisch mit EH tut. Zum Beispiel kann eine solche Funktion eine temporäre Datei öffnen, die sie schließen muss, bevor sie von der Funktion zurückkehrt, egal was passiert, oder einen Mutex sperren, den sie entsperren muss, egal was passiert.
Dafür könnte ich den Zorn vieler Programmierer aus allen möglichen Sprachen hervorrufen, aber ich halte den C ++ - Ansatz für ideal. Die Sprache führt Destruktoren ein, die deterministisch aufgerufen werden, sobald ein Objekt den Gültigkeitsbereich verlässt. Aus diesem Grund muss C ++ - Code, der z. B. einen Mutex durch ein Mutexobjekt mit Gültigkeitsbereich mit einem Destruktor sperrt, diesen nicht manuell entsperren, da er automatisch entsperrt wird, sobald das Objekt den Gültigkeitsbereich verlässt, unabhängig davon, was passiert (auch wenn eine Ausnahme vorliegt) angetroffen). Es ist also wirklich kein gut geschriebener C ++ - Code erforderlich, um jemals mit der Bereinigung lokaler Ressourcen fertig zu werden.
In Sprachen ohne Destruktoren müssen sie möglicherweise einen finally
Block verwenden, um lokale Ressourcen manuell zu bereinigen. Das heißt, es ist immer noch besser, Ihren Code mit manueller Fehlerausbreitung zu verunreinigen, vorausgesetzt, Sie müssen nicht catch
überall Ausnahmen machen.
Umkehrung äußerer Nebenwirkungen
Dies ist das am schwierigsten zu lösende konzeptionelle Problem. Wenn eine Funktion, unabhängig davon, ob es sich um einen Fehlerfortpflanzer oder einen Fehlerort handelt, externe Nebenwirkungen verursacht, muss sie diese Nebenwirkungen zurücksetzen oder "rückgängig machen", damit das System wieder in einen Zustand versetzt wird, als ob der Vorgang niemals statt eines " half-valid "Zustand, in dem die Operation zur Hälfte erfolgreich war. Ich kenne keine Sprachen, die dieses konzeptionelle Problem viel einfacher machen, mit Ausnahme von Sprachen, die lediglich die Notwendigkeit reduzieren, dass die meisten Funktionen externe Nebenwirkungen verursachen, wie funktionale Sprachen, die sich um Unveränderlichkeit und beständige Datenstrukturen drehen.
Dies finally
ist wohl die eleganteste Lösung für das Problem in Sprachen, die sich um Veränderbarkeit und Nebenwirkungen drehen, da diese Art von Logik häufig sehr spezifisch für eine bestimmte Funktion ist und sich nicht so gut auf das Konzept der "Ressourcenbereinigung" abbilden lässt ". Und ich empfehle, finally
in diesen Fällen großzügig zu verwenden, um sicherzustellen, dass Ihre Funktion Nebenwirkungen in Sprachen umkehrt, die sie unterstützen, unabhängig davon, ob Sie einen catch
Block benötigen oder nicht (und wenn Sie mich fragen, sollte gut geschriebener Code die Mindestanzahl von haben catch
Blöcke, und alle catch
Blöcke sollten sich an Stellen befinden, an denen dies am sinnvollsten ist, wie im obigen Diagramm in Load Image User Command
).
Traumsprache
IMO finally
ist jedoch nahezu ideal für die Umkehrung von Nebenwirkungen, jedoch nicht ganz. Wir müssen eine boolean
Variable einführen , um Nebenwirkungen im Falle eines vorzeitigen Abbruchs (von einer ausgelösten Ausnahme oder auf andere Weise) effektiv rückgängig zu machen:
bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
Wenn ich jemals eine Sprache entwerfen könnte, wäre mein Traum, dieses Problem zu lösen, die Automatisierung des obigen Codes:
transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
... mit Destruktoren, um die Bereinigung lokaler Ressourcen zu automatisieren, sodass wir sie nur benötigen transaction
, rollback
und catch
(obwohl ich vielleicht noch hinzufügen möchte, wenn finally
wir beispielsweise mit C-Ressourcen arbeiten, die sich nicht selbst bereinigen). Allerdings finally
mit einer boolean
Variable ist die nächste Sache , dies einfach zu machen , dass ich gefunden habe , so weit meine Traumsprache fehlt. Die zweitkomplizierteste Lösung, die ich dafür gefunden habe, sind Scope Guards in Sprachen wie C ++ und D, aber ich fand Scope Guards konzeptionell immer etwas umständlich, da sie die Idee der "Ressourcenbereinigung" und "Umkehrung von Nebenwirkungen" verwischen. Meiner Meinung nach sind das sehr unterschiedliche Ideen, die auf andere Weise angegangen werden müssen.
Mein kleiner Wunschtraum einer Sprache würde sich auch stark um Unveränderlichkeit und dauerhafte Datenstrukturen drehen, um das Schreiben effizienter Funktionen, die keine umfangreichen Datenstrukturen in ihrer Gesamtheit kopieren müssen, obwohl die Funktion dies bewirkt, zu vereinfachen, obwohl dies nicht erforderlich ist keine Nebenwirkungen.
Fazit
Abgesehen von meinen Ramblings finde ich, dass Ihr try/finally
Code zum Schließen des Sockets in Ordnung und großartig ist, wenn man bedenkt, dass Python nicht das C ++ - Äquivalent von Destruktoren hat, und ich persönlich denke, dass Sie das großzügig für Orte verwenden sollten, die Nebenwirkungen umkehren müssen und minimieren Sie die Anzahl der Stellen, an denen Sie arbeiten müssen, catch
zu Stellen, an denen dies am sinnvollsten ist.