Mit Exceptions werden Ausnahmesituation in einem Programmablauf gekennzeichnet. D.h. es handelt sich nicht zwangsläufig um einen Fehler bzw. um etwas "Schlechtes". Je nach Programmierstil gibt es auch mehr oder weniger der "guten Exceptions". Der Umgang mit Exceptions (= Exception handling) ist für die Stabilität und Zuverlässigkeit von Programmen entscheidend. Ein sauberes und konsequentes Exceptions-Handling ist also kein nice-to-have sondern PFLICHT!
In diesem Artikel sollen die Grundlagen von Exceptions in Delphi/Pascal sowie erweiterte Möglichkeiten aufgezeigt werden.
Exceptions in Delphi
Delphi war eine der ersten Sprachen, welche sogar OS-Exceptions abfangen bzw. handeln konnte - das war ein riesen Schritt in der Compiler-Welt. In Delphi werden die Exceptions in Form von Klassen implementiert. Diese leiten voneinander ab und ergeben somit eine Klassenhierarchie von Exception, mit der Klasse Exception als Mutterklasse aller Exceptions. Alle weiteren Ableitung beginnen mit dem Prefix "E" für Exception (im Vergleich für "T" bei den "normalen" Klassendefinitionen.
Beispiel: Klassenhierarchie für die "Division durch Null": TObject => Exception => EMathError => EZeroDivision
Basisklasse Exception
Die Basisklasse Exception hat bereits einige nützliche Grundeigenschaften, welche somit alle Exceptions habe. z.B. hat JEDE Exception ein Property MESSAGE vom Typ String. Dieses Property beinhaltet den Fehlertext, den ein Anwender am Bildschirm sehen würde, wenn das Programm die Exception nicht abfängt. Kann zum Beispiel für eigene Messageboxen verwendet werden, wenn man die Originalmeldung auch mit ausgeben will.
Daneben hat jede Exception auch Methoden. Die wichtigsten sind wohl die Konstruktoren um ein Exceptionobjekt abzuleiten. Bei Create(const Msg: string) kann die Fehlermeldung als String-Parameter übergeben werden. Etwas komfortabler ist der CreateFmt(const Msg: string; const Args: array of const). Bei diesem alternativen Konstruktor handelt es sich um eine Kombination aus dem Create und einem Format. Details zur Anwendung und dem Formatierungssyntax findet Ihr in der Online Hilfe bei der Format-Funktion.
Exceptions handeln
Wohl eine das wichtigste Kapitel für die tägliche Programmierung in Delphi ist das ordentliche behandeln von Exceptions. Sei es Exceptions vom OS, von der VCL, von der DB oder auch den eigenen Exception. Generell sind alle Exceptions gleich in der Handhabung. D.h. Delphi macht keinen Unterschied zwischen selbstdefinierten Exception jenen von Delphi.
Beim "handeln" von Exception geht es darum, in Eurem Programmcode richtig auf die "Ausnahmesituationen" zu reagieren, sodass das Programm weiter Kontrolliert abläuft und sei es nur, dass es noch Kontrolliert aufräumt ohne das Memory-Leak's entstehen. Weiter darf durch das Auftreten von Exception weder Datenverlust noch Datendefekte entstehen. Dies alles kann man sicherstellen, indem man ein sauberes Exceptions-Handling implementiert.
Mit dem Exception-Handling werden demzufolge Programmblöcke abgesichert und zu logischen Einheiten zusammengefasst.
Welche Programmblcke sind unbedingt mit einem Exception-Handling abzusichern?
Kurz und bündig (Minimum):
- I/O-Blöcke (Files, Socket-Kommunikation, etc.)
- DB-Zugriffe
- Blöcke welche Memory auf dem Heap allozieren (z.B. durch lokale Objekte oder manuelles Memory allozieren etc.)
- Blöcke die mit Synchronisation arbeiten (Multithreading)
- usw.
In Delphi gibt es für das Exception-Handling zwei Code-Konstrukte.
TRY/FINALLY
Der Try-Finally ist wohl das häufiger verwendete Konstrukt.
try
{ Hier kommt der Code rein, der unter Umstnden crashen knnte. }
finally
{ Und hier der Code, der auf jedenfall ausgefhrt wird, auch wenn
der Code zwischen TRY-FINALLY crasht. }
end;
Der Code zwischen TRY und FINALLY wird soweit ausgeführt, bis er a) ohne Fehler durchgelaufen ist, oder b) crasht. Auf JEDENFALL wird aber der Teil zwischen FINALLY und END durchlaufen - auch wenn das rundum die Welt untergehen mag - der FINALLY kommt immer dran.
Mit Hilfe von TRY-FINALLY wird normalerweise der Code im Try-Block abgesichert, indem im Finally sämmtliche "Aufräumaktionen" durchgeführt werden. D.h. auch wenn der Code crasht, werden mind. Locks sowie Memory freigegeben, Files geschlossen etc.
Ein Beispiel:
var
MyObject : TMyClass; // Lokales Objekt deklarieren
begin
MyObject := TMyClass.Create; // Objektinstanz von Klasse erzeugen
try // Von hier an den Block sichern
MyObject.DoSomething; // Irgendwelche Verarbeitung, welche
// theoretisch crashen knnte
finally // Hier wird der "Aufrumteil" eingeleitet
if assigned(MyObject) then // Falls das Objekt noch im Mem ist ...
MyObject.Free; // .. dieses freigeben.
end; // Ende des gesicherten Programmblockes
end;
Das obige Beispiel stellt sicher, dass Memory, welches durch das Objekt MyObject belegt wird, am Ende des Programmblockes wieder freigegeben wird und somit kein Memoryleak entstehen kann - auch nicht, wenn innerhalb des Programmblock ein Fehler (Absturz) auftritt.
TRY/EXCEPT
Der Try-Except ist das zweite Konstrukt, welches Delphi uns für das Exceptions-Handling zur Verfügung stellt. Der Try-Except ist dem Try-Finally nicht nur vom Namen her ähndlich:
try
{ Code der crashen knnte }
except
{ Code der nur dann aufgerufen wird,
wenn oben eine Exception auftritt. }
end;
Wiederum wird der Try-Block ausgeführt. Aber nur wenn innerhalb des Try-Blockes eine Exception auftritt, wird der Except-Block aufgerufen. In diesem kann dann explizit auf die Exception reagiert werden.
Beispiel:
var
f : TextFile;
begin
try
AssignFile(f, 'c:test.txt');
Rewrite(f);
WriteLn(f, 'Zeile 1 der Textdatei');
WriteLn(f, 'Zeile 2 der Textdatei');
CloseFile(f);
except
ShowMessage('Sorry - da ging was in die Hose mit der Datei - TRY AGAIN.');
end;
end;
In dem Beispiel lassen wir den Anwender noch wissen, dass da etwas falsch lief, damit er informiert ist.
Kombinationen
Natürlich könnt Ihr die Try/Finally/End-Konstrukte beliebig tief verschachteln. So würde folgender Code z.B. Sinn machen:
var
f : TextFile;
begin
try
AssignFile(f, 'c:test.txt');
Rewrite(f);
try
WriteLn(f, 'Zeile 1 der Textdatei');
WriteLn(f, 'Zeile 2 der Textdatei');
finally
CloseFile(f);
end;
except
ShowMessage('Sorry - da ging was in die Hose mit der Datei');
end;
end;
Hiermit stellen wir sicher, dass das File auf jeden fall wieder geschlossen wird, wenn es einmal geöffnet werden konnte. Zusätzlich fangen wir die Exception mit dem Except ab und geben eine eigene Meldung an den Anwender raus.
Erweitertes Exception-Handling
Aufbauend auf dem grundlegenden Exception-Handling gibt es noch einige interessant Möglichkeiten mehr.
Except-Block
Speziell der Try-Except hat noch einige zustzliche Mglichkeiten, welche ich Euch vorhin einfach unterschlagen habe Schaut Euch mal folgendes Beispiel aus OH an:
try
X := Y/Z;
except
on EZeroDivide do HandleZeroDivide;
end;
Ihr werdet sehen, das es hier ein neues Schlüsselwort gibt: ON/DO. Dieses Schlüsselwort ist nur innerhalb des Except-Blockes zulässig. Mit dem ON/DO könnt Ihr auf einfache Weise abfragen, ob es ein Bestimmter Exception-Typ (=Exceptionklasse) war, der aufgetreten ist. Im obigen Beispiel wird also im Falle einer "Division durch Null" der Methode HandleZeroDivide aufgerufen, welche den Fehler handeln soll (oder mind. könnte).
Von den ON/DO-Konstrukten kann man beliebig viele hinter einander schachteln. Somit könnt Ihr gezielt auf verschiende Exceptionklassen reagieren - je nachdem wie's für die eine oder andere Klasse angebracht ist. Auch hierzu wieder ein
Beispiel aus der OH:
try
...
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
else HandleAllOthers;
end;
Achtet darauf, dass in diesem Beispiel der EMathError nach den EZeroDivide abgefangen wird. Bei abgeleiteten Exceptionklassen muss natürlich erst die spezialisiertere Klasse vor der allgemeineren Klasse abgefragt werden.
In dem Beispiel seht Ihr auch noch etwas weiteres: der ELSE. Ist die Exception keine der explizit abgefragten Exceptionklassen, dann wird der Else-Teil aufgerufen und somit die Methode HandleAllOthers ausgeführt.
Ein weiteres Beispiel:
try
...
except
on e: Exception do
ShowMessage('Exception aufgetreten.'+#13+
'Exceptionmeldung:'+#13+
e.Message);
end;
Dieses Beispiel ist ähndlich den vorhergehenden, nur das hier gleich das Exception-Objekt der Objektvariable E zugewiesen wird. Mit dieser Objektvariabel kann nun innerhalb des Except-Blockes gearbeitet werden.
Globaler Exception-Handler
Ja, Ihr lesst richtig - da ist noch mehr zum Thema Exceptions. Delphi bietet die Möglichkeit einen globalen Exception-Handler zu hinterlegen. Dies geschieht in Form eines Events auf dem Application-Objekt. Das Application-Objekt hat ein Event namens OnException. Ihr könnt dieses Event entweder mit Hilfe des Komponente TApplicationEvents oder zur Laufzeit per Sourcecode zuweisen.
Bei dem Event bekommt Ihr folgende Parameter mit ber:
procedure OnException(Sender: TObject; E: Exception);
Wie Ihr seht, bekommt Ihr also die Exception selbst als Paramter E berreicht und könnt auf alle Eigenschaften dieser Exception zugreifen. Mit Hilfe des RTTI (Runtime Type Information = Reflections im Java) könnt Ihr wiederum abfragen, um was für eine Exceptionklasse es sich genau handelt.
Auslösen von Exceptions
Wie werden Exceptions eigentlich ausgelöst? Gaaaaanz einfach. Es sind im wesentlichen zwei Schritt nötig:
- Instanzieren eines Exception-Objektes von der gewünschten Exception-Klasse
- Das instanzierte Exception-Objekt als Exception auslösen (raisen)
Beispiel:
Nehmen wir an, wir wollen eine Exception der Klasse EMathError auslösen, da eine Berechnung in die Hose ging. Eine ausführliche Variante könnte folgendermassen aussehen:
var
E: EMathError;
begin
{ Irgend etwas berechnen wobei ein Fehler auftritt }
{ Exception-Objekt erstellen }
E := EMathError.Create('Die Berechnung ging schief!');
raise e;
end;
Da die meisten (inkl. mir) aber etwas Tippfaul sind, geht das auch in einer Zeile und erst noch ohne eigene Variable. Die folgende Variante werdet Ihr in der Praxis normalerweise finden:
begin
{ Irgend etwas berechnen wobei ein Fehler auftritt }
{ Exception-Objekt erstellen }
raise EMathError.Create('Die Berechnung ging schief!');
end;
Das war schon die ganze Hexerei, wie man Exception auslösst.
Eigene Exception-Klassen
Natürlich können auch eigene Exceptionklassen abgeleitet werden. Dies ist bei Komponenten oder Modulen sogar sehr zu empfehlen. Hier ein Beispiel, welches eine neue Exception-Klasse EMyError einführt (abgeleitet von Exception). Hier die Klassendeklaration:
EMyError = class(Exception)
private
FResultCode: TMyResultCode;
FRaisedBy: TMyObject;
public
property RaisedBy: TMyObject read FRaisedBy write FRaisedBy;
property ResultCode: TMyResultCode read FResultCode write FResultCode;
constructor Create( _raisedBy: TMyObject; _code: TMyResultCode; const _msg: string );
constructor CreateCode( _raisedBy: TMyObject; _code: TMyResultCode );
constructor CreateWithInfo( _raisedBy: TMyObject; _code: TMyResultCode; const _info: string );
end;
Es wird also eine neue Klasse namens EMyError von der Basisklasse Exception abgeleitet. Diese neue Klasse bekommt zwei neue Eigenschaften (Properties) eingepflanzt: RaisedBy und ResultCode. Wir verwenden diese zusätzlichen Properties, um zusätzliche Fehlerinformationen - wie Fehlercodes - zu speichern. Dann werden noch einige Konstruktoren überschrieben, damit die zusätzlichen Eigenschaften diesen als Parameter übergeben werden können.
Diese neue Exceptionklasse kann auch als Basisklasse für weitere Exceptionklassen verwendet werden.
EMyDBError = class(EMyError) end;
EMyCalcError = class(EMyError) end;
Eigene Exceptionklassen können ganz normal in Delphi ausgelösst und abgefangen werden, wie alle anderen Exceptions auch (vgl. "Auslösen von Exceptions").