Exceptions Diskussion
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
- Könnte bitte jeder vor seine Beiträge ein "Java: " bzw. "C++: " schreiben, wenn sie sich nur auf eine Sprache beziehen. Dann kann man es leichter einordnen, danke.
- Exception
- engl. "Ausnahme"
Exceptions sind - wie Klassen - ein weiteres Sprachmerkmal zur Unterstützung des strukturierten Programmierens. Frei nach "optimise the common case" muss man nicht mehr für jede Ausnahme eine eigene ad-hoc Signalisierung und Behandlung programmieren sondern kann in weiten Strecken des Sources Ausnahmen ignorieren.
Dabei ist natürlich die Definition von "Ausnahme" sehr schwammig. Liest man eine ganze Datei ein, dann ist EOF eine natürliche Abbruchbedingung. Liest man einen Record mit fixer Länge ein, so ist ein frühzeitiges EOF eine Ausnahme.
Vorteile von Exceptions:
- Keine Spezialfälle: Funktion kann den gesamten Wertebereich des Rückgabetyps als Bildmenge verwenden
- Übersichtlicher: Fehlerbehandlung findet gesammelt statt
- Weniger Aufwand:
- Aufrufer muss nicht manuell Prüfen
- Java: finally-Block kann gemeinsam für Erfolgs- und Fehlerfall benutzt werden
- Informationsreichtum: Fehlerursache kann detailliert beschrieben werden
- Sicherheit:
- Stack wird aufgeräumt
- Exceptions sind fail closed, das Ignorieren von Rückgabewerten ist fail open
Einige Sprachen (wie Java) verlangen eine Deklaration von Exceptions:
- Offensichtlicher: Exceptions können nicht ignoriert werden
- Überprüfbarkeit: Funktionen können nur deklarierte Exceptions werfen und müssen alle anderen behandeln
Kontroversielle Punkte von Exceptions:
- Systematisches Vorgehen: um gebundene Ressourcen im Fehlerfall korrekt freizugeben
- Programmabbruch: durch unbehandelte Exceptions
Nachteile von Exceptions:
Siehe EinGutesExceptionBeispiel und EinSchlechtesExceptionBeispiel.
Wie kann ich beim einem Java/C++-Programm ausschließen, dass es zu einer Programmabbruch mit Stack-Trace wegen einer "uncaught" exception kommt? Ich möchte Fehler so behandelt wissen, dass das Programm sinnvoll darauf reagiert und jedenfalls am Leben bleibt. Es mag sein, dass ein Abbruch bei einem Utility-Programm sinnvoll ist, aber bei einem Benutzerprogramm, oder bei irgendeiner Form von "Serverprogramm", das vielerlei Funktionalitäten hat, ist so ein Programmabbruch inakzeptabel.
Mit
können an passender Stelle alles was ge-throw-ed werden kann auch wieder gefangen werden. Mit C++ heisst das
Für weitere Fragen sei auf http://java.sun.com/docs/books/tutorial/essential/exceptions/index.html verwiesen.
Contra: Exceptions können von Entwicklern viel leichter als Rückgabewerte ignoriert werden, da sie sich ja auf den Standpunkt stellen dürfen, die Exceptions würden "auf höherer Ebene" behandelt werden können. Bei Error-Returncodes ist dagegen die Verantwortlichkeit völlig klar, und wenn jemand einen Error-Returncode ignoriert, hat man gleich den Schuldigen.
- Riposte: Dafür kann der Ursprung einer ungefangenen Exception punktgenau ermittelt werden. Bei einem ignorierten Rückgabewert kann der Fehler unter Umständen erst viel später sichtbar werden.
- Todesstoß: Exceptions können nicht ignoriert werden. Eine nicht abgefangene Exception führt zum Abbruch der momentanen Aktion, ggf. bis hin zum Abbruch des Programms. Der Abbruch erfolgt wohldefiniert, zumindest in C++ werden dabei u.a. auch die anwendungsspezifischen Aufräumarbeiten erledigt (Dank deterministischer Destruktoren). Ein ignorierter Fehlercode dagegegen führt zu potentiell undefiniertem Verhalten. Um eine vergleichbare Situation herzustellen, muß man Exceptions unbehandelt abfangen. Dafür muß explizit Sourcecode geschrieben werden, der klar erkennbar aussagt: "Hier werden Exceptions gefangen, aber nicht weiter behandelt."
- Re: Todesstoß: Leider nicht zuende gedacht. Es sei zunächst gegeben eine Programmsequenz wie sie der Vorredner als mögliches Ergebnis einer Überlegung (durchaus vernünftigerweise) herausstellt. Pseudocode: "try { a(); } catch (...) { /* Hier werden Exceptions gefangen, aber nicht weiter behandelt. */ }". Bis hierhin hat der Vorredner recht, die Exception konnte nicht ignoriert werden, sondern es musste explizit Sourcecode geschrieben werden, der das absichtliche Ignorieren klar erkennbar macht. So. Jetzt geht etwas Zeit ins Land. Der nächste Programmierer ergänzt den Code. Nach a() ist noch b() zu tun. Sagen wir, a() und b() haben gleiche/ähnliche Exceptionsignaturen. Ok, der Zweitprogrammierer ergänzt also "b();" und wir haben "try { a(); b(); } catch (...) { /* Hier werden Exceptions gefangen, aber nicht weiter behandelt. */ }". Im Ergebnis haben wir den bei "Contra" notierten Effekt: Der Entwickler hat die Excpeption von b() auf leichteste Weise ignorieren können, möglicherweise unbeabsichtigt. (In diesem Pseudobeispiel wird aufgrund der Kürze dem Zweitprogrammierer sein [evtl. unbeabsichtigtes] Ignorieren auffallen. In Echtcode kann der try-Block gerne umfangreicher oder in einer äußeren Funktion sein, so dass es ihm vielleicht nicht auffällt. Wie es im Google C++ Style Guide heißt: "More generally, exceptions make the control flow of programs difficult to evaluate by looking at code: functions may return in places you don't expect. This results maintainability and debugging difficulties. You can minimize this cost via some rules on how and where exceptions can be used, but at the cost of more that a developer needs to know and understand.")
Contra: Exceptions vermindern die Übersichtlichkeit des Programms, weil die Fehlerbehandlungen nun nicht mehr nah am Ort des Entstehens erfolgen - wo man den Zusammenhang also noch einigermaßen überblicken würde, sondern an ganz anderer Stelle, wo der Zusammenhang nicht mehr unmittelbar zu sehen ist.
- Riposte: Vergleiche
| int open(...) {
/* ... */
if (fehler) {
errno = error_code;
return -1;
}
/* ... */
if (!(file = open(...)))
{
perror("file open:");
/* ... */ |
|
|
- und
| void open(...) {
/* ... */
if (fehler) {
raise SomeException(error_code);
}
/* ... */
try {
file = open(...);
} catch (SomeException e) {
e.perror("File open:")
/* ... */ |
|
|
- Für so einfache Aufgaben bleibt die Fehlerbehandlung nahe an der Fehlersignalisierungsstelle.
- Wenn die Schere zwischen Signalisierungs- und Behandlungsstelle weiter auseinanderklafft wird es mit Rückgabewerten immer schwieriger. Beispiel: ein Thread, der eine Verbindung eines Webservers bearbeitet, verliert die Netzwerkverbindung zu seinem Client. Erkannt wird das in den Untiefen des Netzwerkcodes. Die Fehlerbehandlung passiert auf höchster Ebene im Thread oder dessen Erzeuger. Während die Exception automatisch durchgereicht wird und die Runtime sich um den Rest kümmert, muss der Rückgabewert durchgereicht werden.
- Fehlersignalisierungsmethoden wie NullObjekt vermeiden vordergründig die schleichenden Laufzeitprobleme von einfach ignorierten Rückgabewerten, verstecken dadurch den Fehler umso gründlicher, da Fehler in der Logik erst als Probleme zweiter Ordnung entdeckt werden.
- Ausnahmesituationen müssen also an drei Stellen bedacht werden:
- Die Stelle, wo der Fehler erkannt wird: Bei Exceptions wird eine Exception geworfen, sonst wird ein Fehlercode zurückgegeben.
- Die Stelle, wo der Fehler behandelt wird: Bei Exceptions wird die Exception gefangen, sonst wird der Fehlercode ausgewertet. Überlädt der Fehlercode den Rückgabewert der Funktion, so muss oft der Rückgabewert im Normalfall in einen passenderen Datentyp umgewandelt werden. Beispiel: getc().
- Überall dazwischen: Ohne Exceptions muss man hier den Fehlercode weiterleiten oder anpassen. Exceptions kann man ignorieren, solange man nicht darauf reagieren will. In beiden Fällen muss man natürlich aufräumen.
Contra: Nochmals zur Übersichtlichkeit: womöglich werden auch noch massig künstliche Exception-Ableitungshierarchieren erfunden, weil man ja die Fehlerinformation nun im Gegensatz zu früher durchreichen muss.
- Riposte: YAGNI, Refaktorisieren, YAGNI, Refaktorisieren, YAGNI, ...
Contra: Gehört nicht Diziplin dazu, nicht einfach try { [...] file.write([...]); [...] } catch(IOException e) {} zu schreiben, wenn der Compiler meckert?
- Riposte: Gehört nicht Disziplin dazu, um lint (siehe LintProgramme) über den Source zu schicken und alle Warnungen punkto missachteter Rückgabewerte zu beachten? Zu beachten ist auch, dass ein leerer catch Block ein Artefakt im Source hinterlässt, während eine fehlende Rückgabewertbehandlung nicht sichtbar ist.
- Eine Anekdote von IljaPreuß:
- "Ein großer Vorteil ist aber, dass ein leerer catch-Block sehr viel auffälliger ist, als ein ignorierter Rückgabewert. Durch Entfernen eines solchen leeren catch-Blocks habe ich gerade letztens einen Programmierfehler in einem kleinen Hilfsprogramm aufgedeckt (der zu einer bis dato ignorierten ArrayIndexOutOfBoundsException geführt hatte). Der Fehler war bereits Monate unbemerkt geblieben, und ich bezweifle, dass ich über den Fehler "gestolpert" wäre, wenn es sich um einen ignorierten Rückgabewert gehandelt hätte."
Vielleicht kann da wer noch sinnvolles Extrahieren?
Exceptions halte ich z. B. für sehr wertvolle Sprachelemente, die die Fehlerbehandlung extrem erleichtern. Außerdem kann man sie z. B. in Basic mit genügend gotos auch nachahmen :-) . An den Schnittstellen z. B. zwischen Bibliothek und Anwendungsprogramm dürfen diese natürlich nicht auftreten. -- mb
- Meine Begeisterung für Exceptions ist nicht so groß, weil sie das Problem der Resourcenfreigabe nicht zu lösen scheinen. -- hl
- Allgemein: Ich weiß nicht genau, was für Probleme Du meinst. Natürlich müssen z. B. Destruktoren (C++) oder finally statements (Java) korrekt programmiert sein. Dann ist es aber wesentlich einfacher als das ständige Abprüfen von Rückgabewerten. -- mb
Ein Vorteil von Exceptions könnte sein, daß, wenn ein unkritisches Problem vorliegen würde (z. B. Speichermangel, ...), das Programm nach Beheben des Problems an derselben Stelle weiterarbeiten könnte, an der das Problem aufgetreten ist. Leider scheinen die Exceptions genau dies nicht zu ermöglichen :-/ (Ich muß da mal wieder einen Blick in mein schlaues Buch werfen) --rae
Kein Verfahren garantiert automatisch fehlerfreie Programmierung oder auch nur die korrekte Behandlung von Fehlern. Nur wenn in der SoftwareEntwicklung darauf ein Schwerpunkt gesetzt wird, kann es mittelfristig eine entsprechende Verbesserung der Qualität geben.
Es ist auch klar, dass in verschiedenen Bereichen Fehler weniger toleriert werden als in anderen. Wenn eine Produktionsmaschine ausfällt, eine Rakete abstürzt, Bankkonten nicht stimmen, Gepäckstücke am Flughafen ihre Orientierung verlieren, dann hat das extremere Folgen, als bei einem wissenschaftlichen Simulationsprogramm, einem Photobearbeitungsprogramm oder einem 3D-Spiel.
In den letzten 10 Jahren (1990-2000) hat die Fehlerhäufigkeit in der Massen-Software zugenommen. In einer Zeit, wo aktualisierte Software-Versionen
über das Internet verteilt werden, werden neuen Hardwarekomponenten (Drucker, Grafikkarten etc.) selten einigermaßen fehlerfreie Treiber oder Utilities mitgeliefert. Nachdem das Usus geworden ist, ist die allgemeine Moral diesbezüglich weiter abgesunken.
OO Methoden (inklusive der Fehlerbehandlung mittels Exceptions) haben an dieser Situation nichts geändert. Ich denke sogar, dass sie die Situation verschlechtert haben.
Nachdem die Fehlererkennung (throw) und die Fehlerbehandlung (catch) von einander isoliert wurden, fehlt jetzt die klare Verantwortlichkeit für die Fehlerbehandlung.
- Wenn ein Fehler nicht dort behandelt werden kann, wo er auftritt, ist die Trennung ganz logisch. --mb
Einerseits werden die vorhandenen Informationen über die Fehlerursache oft nicht aufgegriffen und weitergegeben (Mängel im throw-Bereich).
Im Zwischenbereich zwischen trow und catch werden Resourcenprobleme oft einfach ignoriert. Dies führt oft zur Situation, dass Programme, die nach Fehlern weiterarbeiten, eine deutlich erkennbare Instabilität aufweisen, sodass es manchmal besser ist, sie zu beenden und neu zu starten.
Im catch-Bereich geht man den Weg des geringsten Aufwandes und gibt sich allenfalls mit einem Stack-Trace als "korrekter" Reaktion auf Fehler zufrieden. Die Möglichkeiten, durch eine möglichst frühe Behandlung eines Fehlers möglichst weitgehende Informationen über das Umfeld der Fehlerursache zu bekommen, werden oft nicht genutzt.
- Wer schlampig arbeitet, arbeitet schlampig, egal ob mit Exceptions oder Fehlercodes. Fehlercodes laden doch noch mehr zum Ignorieren ein. --mb
Die Ursachen für die - von mir behauptete - Misere sehe ich in zwei Faktoren.
Der erste ist vermutlich unbestritten: korrekte Programme bzw. eine optimale Behandlung von Fehlern kostet viel Zeit und Geld. Die meisten Firmen sind in einer Situation, wo sie sich beides nicht leisten können. Der zweite ist vermutlich auch unbestritten. Die meisten Firmen haben keine "Reuse"-Strategie und produzieren ihre Software für den Anlassfall. Dies führt auch zur Meinung WardsWiki:ReuseHasFailed.
Der Aufwand für "fehlerarme" Software würde sich wohl lohnen, wenn die Softwareproduzenten eine umfassende Strategie zur Wiederverwendung der erzeugten Software hätten. Das kann aber nur eine Wiederverwendung fehlerfreier "Libraries" sein, eine "Cut-Paste-Rape" Wiederverwendung relativ fehlerhaften Codes ist problematisch.
- Ich kann nur von Java sprechen, und da gibt es einige Probleme. Objekte werden zwar finalisiert, aber wann und in welcher Reihenfolge ist undefiniert. Ein offener File *wird* beispielsweise geschlossen, aber dann wenn die GarbageCollection das nicht mehr benötigte Objekt "einsammelt". Wird der File gelöscht, falls es nur ein temporär benötigter File ist (vermutlich nicht)? Was passiert mit gelockten Filebereichen (nehmen wir an, wir machen das über JNI). Ich kann mir viele Dinge vorstellen, die beim Exception-handling Probleme machen können und die als "Sonderfälle" kaum je ordentlich getestet werden.
- Es gibt aber normalerweise einen definierten Punkt, an dem die Datei sowieso geschlossen werden soll, Filebereiche freigegeben werden usw.
- Wenn da also z. B. vorher stand
| <Öffne Datei>
<Tue etwas damit>
<Teste/Behandle Fehler>
<Schließe Datei> |
|
|
- wird daraus
| <Öffne Datei>
try {
<Tue etwas damit>
}
catch(...) {
<Behandle Fehler>
}
finally {
<Schließe Datei>
} |
|
|
-- mb
- Wo ist dann eigentlich die Arbeitsersparnis der Exceptions gegenüber:
| <Öffne Datei>
error=<Tue etwas damit>
if(error) {
<Behandle Fehler>
}
<Schließe Datei> |
|
|
-- hl
- Wenn die Funktion <Tue etwas damit> selbst mehrere Funktionsaufrufe enthält und diese wieder Aufrufe von Unterfunktionen, spart man sich eine ganze Menge von Prüfungen auf Fehler, die dann nach oben durchgereicht werden müssten. Eine Exception wird von alleine weitergereicht und falls keine weiteren speziellen Ressourcenbehandlungen nötig sind, entfällt der Code zur Fehlerbehandlung in vielen Unterfunktionen ganz. --mb
- Ich sehe immer mehr Probleme als Nutzen. Mir begegnen in Java unangenehme Runtime-Fehler. Wenn Exceptions nicht ordentlich behandelt werden, können Sie einigen Schaden anrichten. Der Absturz der Ariane-5 war - soweit ich mich erinnere - z. B. auf einen solchen Fall zurückzuführen. Wahrscheinlich liegt es an meiner vorsichtig-defensiven Programmierart, ich schreibe lieber etwas mehr Code, und bin auf der sicheren Seite. Aber vielleicht fehlt nur EinGutesExceptionBeispiel.
- Moment --- ich habe es zwar nicht ganz im Kopf, aber bei Ariane war ganz sicher keine Java-Exception im Spiel. Eher ein nicht abgefragter Rückgabewert. Ich kann den Bericht villeicht zu Hause mal raussuchen, falls Du es genau wissen willst. --rae [Die Verbindung (Ariane-Exception-Problem)+(Java) war nicht beabsichtigt, ich weiß nicht mehr, in welcher Sprache programmiert wurde]
- Nächstes mal kommt halt wieder ein ;-) dazu ;-)
- Zum Ariane5Absturz.
- Das Problem der Ariane war also nicht das Auftreten einer Exception, sondern das inadäquate Behandeln einer Exception.
- Mit Exceptions kann man genauso 'vorsichtig-defensiv' programmieren, wie ohne - nur dass mit im Allgemeinen halt etwas weniger Code benötigt wird. Außerdem wird der Code durch Exceptions schlanker und besser lesbar, da eben nicht jede zweite Zeile sich mit Fehlerbehandlung beschäftigen muss...
Was Exceptions angeht, mal meine Sicht der Vor- und Nachteile (C++)
- Vorteile
- Bei der Verwendung von Exceptions muß man nicht bei jeder Funktion, die in dem Try-Block aufgerufen wird, den Rückgabewert kontrollieren (Ich kenne nur wenige Entwickler, die alle Rückgabewerte explizit prüfen; die meisten gehen mehr oder weniger von der Annahme aus, daß hier und da kein Fehler entstehen kann).
- Der Quellcode wird übersichtlicher, weil die Fehlerbehandlung an einer definierten Stelle stattfindet
- Der Stack wird korrekt aufgeräumt
- Java erzwingt im Gegensatz zu C++ die Verwendung von Exceptions, wenn welche definiert wurden
- Nachteile
- Zur Laufzeit allocierter Speicher wird nicht automatisch freigegeben. Das gleiche gilt für andere benutzte Resourcen.
- Für Speicher gibt es Auto-Pointer oder andere Smart Pointer, die für eine solche Freigabe sorgen. Für alles andere gibt es Destruktoren. --mb
- Sicher kann man das Problem Programmtechnisch in den Griff kriegen, die Frage ist nur, mit welchem Aufwand.
- Bevor wir aneinander vorbeireden: Die Aufräumaktion bei den Speicher-Resourcen kann das System nur auf dem Stack machen, weil nur dort definitiv bekannt ist, was mit diesen Variablen geschehen soll. Dabei werden natürlich auch die Destruktoren von lokalen Klassen aufgerufen. Dummerweise kann man Klassen auch dynamisch mittels new anlegen, die sich dann im Static-Memory befinden, nicht auf dem Stack. Dadurch überleben diese Variablen auch, wenn die Funktion, in der sie angelegt wurden, beendet wird. Normalerweise sind diese Variablen dann die Rückgabeparameter der entsprechenden Funktion und werden von der übergeordneten Funktion, die diese Variablen benutzt, auch ordnungsgemäß gelöscht. Und genau da hackt es bei den Exceptions --- wenn in einer Funktion eine Exception geworfen wird, wird sie irgendwo aufgefangen. Sie ist im Prinzip nichts anderes, als ein goto zu einer definierten Adresse, bei dem der Stack aufgeräumt wird, indem so nebenbei für jede passierte Funktion deren 'Destruktor' aufgerufen wird. Dabei werden die Codeteile übersprungen, welche die Variablen in dem Static-Memory löschen sollten, was Probleme verursachen könnte.
- Genau weiß ich nicht, was Du meinst. Jedenfalls kann man Auto-Pointer auch ganz normal als Rückgabewert verwenden. Deren Destruktor wird bei einer Exception dann auch aufgerufen. --mb
- Das ist eine der wichtigeren Regeln für Exception-sichere Programme in C++: "Resource Allocation is Instantiation". Es werden keine Resourcen angefordert, die nicht so in ein Objekt (auf dem Stack) verpackt sind, dass sie wieder freigegeben werden, wenn dieses Objekt wieder verschwindet. Dann ist es nicht wichtig, ob das Objekt verschwindet, weil der Gültigkeitsbereich des Objekts verlassen wird oder weil der Stack wegen einer Exception aufgerollt wird. Rückgabewerte werden per Kopierkonstruktor in ein neues Objekt kopiert, vor die lokale Variable, die den Wert des Rückgabewertes enthält, ihren Gültigkeitsbereich verlässt. Referenzzählung ist dabei eine (meist notwendige) Optimierung. -- KurtWatzka
Siehe auch
KategorieDiskussion
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Text dieser Seite ändern (zuletzt geändert: 19. Dezember 2008 15:52 (diff))