Nutzen von Exceptions
- Überblick
- Compilermeldungen
- Noch ein paar Zusatzinfos zu Compilermeldungen
- Exceptions
- Nutzen von Exceptions
- Fazit
Wir haben nun kennen gelernt, wie man Exceptions vermeidet. Jetzt stellt sich die Frage, für was es überhaupt Exceptions gibt, wenn man sie eh vermeiden sollte. Das Konzept der Exceptions existiert in vielen Programmiersprachen und fußt auf der objektorientierten Programmierung(OOP). Bevor es Exceptions gab, hat man im Wesentlichen mit zwei Systemen zur Fehlerbehandlung gearbeitet: Fehlercodes und Fehlerreportfunktionen. In der WinAPI, also der Funktionssammlung von Windows, welche nun zunehmend vom .NET-Framework(welches Exceptions benutzt) ersetzt werden soll, kommt man noch mit beiden Typen in Kontakt.
Fehlercodes
Wird eine Funktion ausgeführt, gibt sie einen Fehlercode(meist einen Integer) zurück, der angibt, ob die Funktion erfolgreich ausgeführt werden konnte(meist ist dann der Fehlercode 0) oder nicht. In letzterem Fall spezifiziert dann der Fehlercode die Art des Fehlers. Speziell dafür definierte Fehlerkonstanten machen dann den Code lesbarer. Das ganze hat allerdings einen Nachteil: Werden Funktionen von Funktionen benutzt, muss der Fehlercode immer „von Hand“ angepasst bzw. durchgereicht werden. Und da, fast jede Funktion ihre eigenen Fehlercodes hat, kann das zu einem riesigen Fehlercodebrei werden, was der Übersicht nicht gerade förderlich ist…
Fehlerreportingfunktionen
Eine Art „Übergangslösung“, wenn man das denn eine „Lösung“ nennen darf, sind die Funktionen vom Typ „RaiseLastOSError“. Tritt ein Fehler in einer Funktion auf, die dieses Konzept verfolgt, so merkt sich Windows diesen Fehler intern. Weiter passiert erstmal nichts. Die Funktion liefert dann nur noch zurück, ob sie erfolgreich ausgeführt wurde, oder nicht(z.B. über einen Rückgabewert vom Typ Boolean). In letzterem Fall kann dann der Programmierer an einer geeigneten Stelle RaiseLastOSError(bzw. früher: RaiseLastWin32Error) aufrufen und dem Anwender wird die Fehlermeldung präsentiert.
Exceptions
Mit der OOP kam dann schließlich ein Umdenken in der Programmierung. Wurden bisher Funktionssammlungen programmiert und diese genutzt(prozedurale Programmierung), so werden nun voneinander unabhängige Programmbausteine(so genannte Objekte; Beispiele: Buttons, Memos…) entwickelt. Diese sind meist unterschiedlichen Schichten zugeordnet (eine untere Schicht z.B. kümmert sich nur um die Daten und ausschließlich die oberste Schicht kommuniziert mit dem User). Je nach Komplexität und Art der Anwendung kann es mehr oder weniger dieser Schichten geben. Problematisch wird es nun, wenn ein Fehler in der untersten Schicht auftritt. Da nur die oberste Schicht mit dem Benutzer kommunizieren darf und aber gleichzeitig die mittleren Schichten im Fehlerfall wissen müssen, was sie zu tun haben (kontrollierter Abbruch einer Aktion), muss die Information, dass ein Fehler(und natürlich auch welcher) ganz unten aufgetaucht ist durch alle Schichten irgendwie nach oben wandern. Teilweise werden dabei auch noch unterschiedliche Informationen gebraucht.
Um den ganzen Fehlerbehandlungsprozess in den Griff zu bekommen, gibt es nun Exceptions. Eine Exception ist aus OOP-Sicht nichts anderes, als eine Klasse, die durch einen speziellen Mechanismus das Durchreichen der Fehlermeldung vereinfacht.
Eine Exception kann unterschiedliche Informationen enthalten und lässt sich daher mit einem Paket vergleichen. Tritt irgendwo ein Fehler auf, wird ein Paket (Exception) erstellt. Diese wird nun automatisch durch die einzelnen Schichten durchgereicht. Jede Schicht kann sich das Paket angucken, etwas rausnehmen, etwas reintun oder das Paket sogar für angekommen erklären (und damit auch nicht mehr weitergeben). So hat dann jede Schicht alle Infos die sie braucht und kann flexibel darauf reagieren. Dabei werden alle weiteren Aktionen abgebrochen, bis die Exception behandelt ist.
Dabei muss eine Exception nicht zwangsweise ein Fehler sein. Wie oben schon erwähnt ist eine Exception eine Art „erweiterter Rückgabewert“, der dem Programmierer die Möglichkeit gibt, Informationen (zu Ausnahmesituationen) aus unteren Schichten bis dorthin durchzureichen, wo sie gebraucht werden. Meistens handelt es sich dann um einen „Fehler“, aber eben nicht immer. Insbesondere bei den Indy-Komponenten führt dies bei manchen Usern von Zeit zu Zeit zu Verwirrung(EIdConnClosedGracefully)…
Wenn man also die Möglichkeit hat, Exceptions zu vermeiden, sollte man das natürlich tun, denn dann spart man sich(bzw. dem Programm) die ganze Paketversendearbeit. Manchmal braucht man aber genauere Informationen bzw. manchmal bietet sich gar keine Möglichkeit die Exception zu verhindern. Und genau für solche Fälle gibt es Exceptions. Wie man nun damit umgeht, sehen wir im nächsten Kapitel…
Exceptions benutzen
Schauen wir uns nun einmal an, wie Exceptions benutzt werden. Dazu dient uns folgendes Grundgerüst:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | procedure TForm1.Button1Click(Sender: TObject); begin if F1 then ShowMessage('Hallo.'); end; function TForm1.F1: Boolean; begin Result := F2('test') <> 0; end; function TForm1.F2(s: string): Integer; begin if s = 'test' then Result := 0 else if s = 'test2' then Result := 1 else Result := -1; F3(Result); end; procedure TForm1.F3(i: Integer); begin // mach was end; |
Die einzelnen Funktionen symbolisieren die einzelnen Schichten des Programmes. Jede Funktion wird von einer anderen aufgerufen. Durch Klick auf den Button werden also alle Funktionen ineinander verschachtelt aufgerufen…
Tritt nun in F3 ein Fehler auf, so wollen wir den Parameter in Button1Click auswerten können. Dazu leiten wir erst einmal eine neue Exception-Klasse ab:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | EMyException = class(Exception) // alle Exceptions beginnen per Konvention mit einem E; Die Klasse Exception ist Vorfahr aller Exceptions private FParam: Integer; public constructor Create(Msg: string; Param: Integer); property Param: Integer read FParam write FParam; end; [...] constructor EMyException.Create(Msg: string; Param: Integer); begin inherited Create(Msg); FParam := Param; end; |
Wir haben also nun unsere eigene Exception-Klasse, die wir um eine weitere Eigenschaft und einen entsprechenden Konstruktor erweitert haben.
Mit der Zeile
1 | raise EMyException.Create('Fehler', i); |
können wir nun die Exception erzeugen(‚werfen‘).
Durch entsprechende try…except-Blöcke kann nun die Exception abgefangen und behandelt werden:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | procedure TForm1.Button1Click(Sender: TObject); begin try if F1 then ShowMessage('Hallo.'); except on E: EMyException do begin ShowMessage('Eine EMyException ist in Button1Click angekommen. ' + 'Der Parameter war ' + IntToStr(E.Param) + sLineBreak + 'Die Meldung lautet:' + sLineBreak + E.Message); end; on Exception do // egal, welche Art von Exception es ist begin // da aber die EMyException oben schon behandelt wurde, passiert hier nichts ShowMessage('Irgendeine Exception ist in Button1Click angekommen; ' + 'Exception wird nicht weiter gegeben'); end; end; end; function TForm1.F1: Boolean; begin try Result := F2('test') <> 0; except on E: Exception do // egal, welche Art von Exception es ist begin ShowMessage('Eine Exception ist in F1 angekommen.'); raise; // weiterreichen end; end; end; function TForm1.F2(s: string): Integer; begin try if s = 'test' then Result := 0 else if s = 'test2' then Result := 1 else Result := -1; F3(Result); except on E: EMyException do // nur, wenn es sich um eine EMyException handelt begin E.Message := 'Meldung aus F2' + sLineBreak + E.Message; // Meldung anhängen raise; // weiterreichen end; end; end; procedure TForm1.F3(i: Integer); begin raise EMyException.Create('Fehler', i); // Exception werfen end; |
Wenn wir das Programm starten, erhalten wir folgende Meldungen:
Project1
—————————
Eine Exception ist in F1 angekommen.
—————————
OK
—————————
und dann:
Project1
—————————
Eine EMyException ist in Button1Click angekommen. Der Parameter war 0
Die Meldung lautet:
Meldung aus F2
Fehler
—————————
OK
—————————
Was passiert aber nun, wenn es sich um eine andere Exception-Klasse handelt? Probieren wirs einfach aus. Oben im Code sind schon Teile vorhanden, die sich darum kümmern. Zum Testen, ändern wir die Zeile
1 | raise EMyException.Create('Fehler', i); // Exception werfen |
in
1 | raise Exception.Create('Fehler'); // Exception werfen |
Dabei erhalten wir folgende Meldungen:
Project1
—————————
Eine Exception ist in F1 angekommen.
—————————
OK
—————————
und danach:
Project1
—————————
Irgendeine Exception ist in Button1Click angekommen; Exception wird nicht weiter gegeben
—————————
OK
—————————
Hier wurde die Exception in F3 geworfen, in F2 automatisch durchgereicht, weil sie dort nicht behandelt wurde, in F1 registriert und weitergereicht und in Button1Click abschießend behandelt.
Schauen wir uns noch einmal Folgendes an:
1 2 3 4 5 6 7 8 9 10 11 | on E: EMyException do begin ShowMessage('Eine EMyException ist in Button1Click angekommen. ' + 'Der Parameter war ' + IntToStr(E.Param) + sLineBreak + 'Die Meldung lautet:' + sLineBreak + E.Message); end; on Exception do // egal, welche Art von Exception es ist begin // da aber die EMyException oben schon behandelt wurde, passiert hier nichts ShowMessage('Irgendeine Exception ist in Button1Click angekommen; ' + 'Exception wird nicht weiter gegeben'); end; |
Was pasiert wohl, wenn wir die beiden on-Blöcke vertauschen? Ganz einfach: Der untere Block wird in keinem Fall ausgeführt. Auch, nicht, wenn es sich um eine EMyException handelt. Die Exception ist ja schon durch den vorhergehenden Block schon behandelt. Wichtig ist also, dass die allgemeinen on-Blöcke unten stehen und die spezifischen oben…
Wir sehen also, dass alle Informationen von ganz unten nach ganz oben wanden können. Dabei kann jede Schicht auf die Infos zugreifen und diese sogar verändern. Trotzdem müssen wir uns weder Gedanken über das Freigeben der Exception-Klasse (das macht Delphi automatisch), noch über das Durchreichen machen, was mit einem einfachen raise; getan war.
Und sollte eine Exception gar nicht behandelt werden (was eigentlich ein Programmierfehler ist), springt Delphi für einen ein und zeigt dem Benutzer die Fehlermeldung, damit der den Support kontaktieren kann… 😉