Ist es falsch, Ausnahmen in Programmiersprachen zu verwenden?
Nein, auf keinen Fall. Das Erkennen einer Fehlersituation (Fehler ist hier das Auftreten einer nicht zum Design gehörenden Situation) soll immer und grundsätzlich im Werfen einer Aushame resultieren.
Beispiel:
Sei
int divide( int zaehler, int nenner )
eine Funktion, die zwei Zahlen übernimmt, eine gnazzahlige Division durchführt und das Ergebnis zurückliefert. Frage: was soll in diesem Fall hier
divide( 23, 0 )
passieren?
Die Standardlösung wäre, eine spezielle Zahl (gerne mal -1 oder -255 oder so) als Marker zu benutzen, um eine solche Situation zu notieren. Die Alternative, das Programm mit einer Fehlermeldung (gerne z.B. "es wurde versucht,m durch 0 zu teilen. Das Programm wird beendet" ) lassen wir mal weg.
Kein Problem soweit. Das Problem tritt beim Benutzer der Funktion divide auf. Neue Programmier kommen, nach Jahren kommt ein Werkstudent und sieht, dass divide genau das macht was er gerade braucht…. er verwendet die Funktion, vergisst aber natürlich, auf den speziellen Fehlercode abzufragen. Da er ansonsten aber ein guter Mann ist, kommt eine versuchte Division durch 0 nicht vor. Nach Jahren verändert ein Dritter Programmierer eine winzige Kleinigkeit an einer anderen Stelle des Programms, und nun passieren seltsame Dinge, die sich niemand erklären kann. Plötzlich rufen Kunden an, dass in der Produktion Löcher an der falschen Stelle gebohrt werden, oder der Ertrag ihrer Solaranlage plötlich negativ angezeigt wird.
Das wirklich Schöne an diese Situation ist, dass der Fehler nicht dort bemerkt wird, wo er entsteht - es ist einfach Unsinn, -255 zurück zu liefern als wäre es ein ganz normales Ergebnis, und die Prüfung darauf dem Benutzer der Funktion aufzubürden. Damit weiter zu rechnen - und das wird garantiert irgendwann passieren - führt dann irgendwo ganz anders und viel später zu anderen Problemen - niemand wird darauf kommen, dass ein um 255 Einheiten nach links versetztes Bohrloch letztendlich auf einen Divide-By-Zero Fehler zurück zu führen ist.
Ein anderes Beispiel aus der Praxis? Gerne!
Wir wollen das Durchschnittseinkommen der Abteilung berechnen. Dazu schreiben wir etwas in der Art wie hier:
sum = 0; 
count = 0; 
foreach( p in Persons ) 
{ 
 sum = sum + p.GetSalary(); 
 count = count +1; 
} 
result = sum / count; 
Habe ich in ähnlichen Aufgabenstellungen so schon hundertausend mal gesehen. Was ich aber auch gesehen habe ist, dass GetSalary() den Wert -1 liefert, wenn das Gehalt nicht bestimmt werden kann - z.B. weil die Person gar nicht in der Abteilung arbeitet.
Auch hier ist der Effekt besonders schön, dass das Programm durchaus Ergebnisse liefert, aber eben nicht immer die korrekten. Mit sowas in einem Produktivsystem weiter zu rechnen, ist tödlich.
Natürlich hätte man das Ergebnis von GetSalary() auf Fehlercodes prüfen müssen. Oder dafür sorgen, dass in der Gruppe 'Persons' nur die richtigen Leute sind. Klar, hätte man müssen…. hat man aber nicht. Es war eben ein Juniorprogrammierer, der den Code zwar getestet hat, aber dass mal eine Person nicht da war… kam bisher nicht vor.
Selbst wenn man Returncodes prüft, ist man nicht sicher. Mit sowas hier kann man in der Zukunft viel Spass haben:
foreach( p in Persons ) 
{ 
 salary = p.GetSalary(); 
 if ( salary < 0 ) 
  return salary; // return errorcode 
 count = count +1; 
} 
Wobei 'salary' hier eigentlich nicht richtig ist, denn es ist ja evtl. auch ein Fehlercode… aber das nur nebenbei. Niemend wird das aber 'salaryOrErrorCode' nennen, oder? Was passiert nun, wenn der Ersteller von GetSalary() nun seine Errorcodes umstellt, detaillierter fasst oder irgendwie ändert? Da sein Benutzer (hier: die Schleife) Fehlercodes nach oben weiter gibt, muss er im Prinzip das ganze Programm verstehen, um die Auswirkungen seiner Änderungen übersehen zu können. Er muss u.a. sehen, welche Fehlercodes andere Programmteile verwenden, um durch seine Änderungen keine Überschneideung zu bekommen. Glaubt mir: das wollt ihr in eueren Programmen nicht wirklich - irgendwann wird es diese Überscheidungen geben…. eine weitere, schöne Quelle für undefined behavior, wie wir dazu sagen.
Ein weiterer, sehr unschöner Effekt von Fehlercodes ist, dass man quasi nach jedem Funktionsaufruf eine Ürüfung auf Fehlercodes machen muss. Dadurch steigt der Umfang des Quellcodes drastisch an - das ginge ja noch, aber es ist unschön, dass die produktiven Teile des Codes von längeren Teilen Fehlerbehandlung unterbrochen werden, die eigentlich nie aufgerufen werden. Wenn man Code verstehen oder warten will, möchte man die Funktionalität sehen, nicht Behandlungsroutinen für Fehler.
Es gibt noch eine Reihe weitere Probleme mit Returncodes, aber dies hier soll keine Vorlesung über das Design von Programmiersprachen oder gutem Code sein. Aber wie soll es denn sein? Ungefähr so:
Eine Funktion hat eine klar definierte Aufgabe, die sie zu erfüllen hat, wenn bestimmte Voraussetzungen (Preconditions) erfüllt sind. Nach der Funktionsasführung müssen dann die Nachbedingunen (Postconditions) erfüllt sein.
Sind also die Preconditions erfüllt, muss die Funktion liefern. Die Postconditions müssen nach Lieferung erfüllt sein.
Sind die Preconditions nicht erfüllt, muss die Funktion nicht liefern. Sie wirft eine Ausnahme. Postconditions müssen dann nicht erfüllt werden. Die Funktion hat nichts (wirklich nichts) verändert, der Zustand des Programms ist wie vor den Funktionsaufruf.
Pre- und Postconditions gehören zum Vertrag (Contract), den die Funktion ihren Benutzern anbietet. Der Contract muss nicht immer explizit dokumentiert werden, häufig reicht implizite Doku aus (z.B. durch geeignete Namensgebung), oder in der Testsoftware (s.u.)
Die Einhaltung der Preconditions muss vom Aufrufer der Funktion sichergestellt werden. Preconditions werden am Anfang der Funktionsimplementierung verifiziert.
Die Einhaltung der Postconditions muss von der Funktion selber sichergestellt werden. Postconditions werden am Ende der Funktion oder manchmal vom Aufrufer der Funktion verifiziert
Contracts werden häufig auch durch Testen verifiziert. Man verwendet dabei neben korrekten Daten auch eben solche, die die Preconditions nicht erfüllen und prüft dann, ob die Funktion eine Ausnahme wirft. Tests für Funktionsvertäge können außerdem ausgezeichnet zur Dokumentation eben dieser Verträge verwendet werden.