Exceptions Diskussion
 
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern

Veränderung (letzte Korrektur) (Änderung, Autor, Normalansicht)

Verändert: 57c57
::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 vieloeicht 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.")
::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.")

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.

Allgemeines

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.

Vor- und Nachteile

Vorteile von Exceptions:

Einige Sprachen (wie Java) verlangen eine Deklaration von Exceptions: Kontroversielle Punkte von Exceptions:

Nachteile von Exceptions:

Anwendungen

Siehe EinGutesExceptionBeispiel und EinSchlechtesExceptionBeispiel.

Fragen

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
catch (Throwable t)
können an passender Stelle alles was ge-throw-ed werden kann auch wieder gefangen werden. Mit C++ heisst das
catch (...)

Für weitere Fragen sei auf http://java.sun.com/docs/books/tutorial/essential/exceptions/index.html verwiesen.

Diskussion

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:

    1. Die Stelle, wo der Fehler erkannt wird: Bei Exceptions wird eine Exception geworfen, sonst wird ein Fehlercode zurückgegeben.
    2. 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().
    3. Ü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."

Rest Schrott

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 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

Einige Gedanken

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.

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.

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.

Java

<Öffne Datei>
error=<Tue etwas damit>
if(error) {
  <Behandle Fehler>
}
<Schließe Datei>
-- hl

C++

Was Exceptions angeht, mal meine Sicht der Vor- und Nachteile (C++)


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))
Suchbegriff: gesucht wird
im Titel
im Text