Exceptions (Delphi)

Aus RAD Studio
Wechseln zu: Navigation, Suche

Nach oben zu Klassen und Objekte - Index

In diesem Thema wird Folgendes behandelt:

  • Überblick über die Konzepte von Exceptions und der Exception-Behandlung
  • Exception-Typen deklarieren
  • Exceptions auslösen und behandeln

Allgemeines zu Exceptions

Eine Exception wird ausgelöst, wenn die normale Programmausführung durch einen Fehler oder ein anderes Ereignis unterbrochen wird. Die Steuerung wird dadurch an eine Exception-Behandlungsroutine übergeben. Mit ihrer Hilfe kann die normale Programmlogik von der Fehlerbehandlung getrennt werden. Da Exceptions Objekte sind, können sie durch Vererbung in einer Hierarchie organisiert werden. Sie bringen bestimmte Informationen (z. B. eine Fehlermeldung) von der Stelle im Programm, an der sie ausgelöst wurden, zu dem Punkt, an dem sie behandelt werden.

Wenn die Unit SysUtils in einer Anwendung verwendet wird, werden die meisten Laufzeitfehler automatisch in Exceptions konvertiert. Viele Fehler, die andernfalls zum Beenden der Anwendung führen (z. B. Speichermangel, Division durch null oder allgemeine Schutzverletzungen), können so abgefangen und behandelt werden.

Einsatzmöglichkeiten für Exceptions

Exceptions ermöglichen das Abfangen von Laufzeitfehlern, die sonst einen Programmabbruch zur Folge hätten oder umständliche Bedingungsanweisungen erfordern würden. Die Semantik der Exception-Behandlung stellt bestimmte Anforderungen, die zu Abzügen in den Bereichen Code-/Datengröße und Laufzeitleistung führen. Zwar kann für fast alle Probleme oder Fehler eine Exception generiert und praktisch jeder Quelltextblock durch eine umgebende try...except- oder try...finally-Anweisung geschützt werden, in der Praxis sollte dieses Vorgehen aber auf Sonderfälle beschränkt bleiben.

Die Exception-Behandlung eignet sich für Fehler, die selten auftreten oder sich nur schwer eingrenzen lassen, die aber schwerwiegende Folgen haben können (z. B. einen Programmabsturz). Sie kann auch für Fehlerbedingungen eingesetzt werden, die sich nur mit großem Aufwand in if...then-Anweisungen testen lassen, oder wenn Sie auf vom Betriebssystem oder Routinen ausgelöste Exceptions reagieren müssen, auf deren Quellcode Sie keinen Zugriff haben. Exceptions werden in der Regel für Hardware-, Speicher-, E/A- und Betriebssystemfehler verwendet.

Bedingungsanweisungen sind oft die beste Methode für einen Fehlertest. Wenn Sie zum Beispiel sicherstellen möchten, dass eine Datei vorhanden ist, bevor Sie versuchen, sie zu öffnen. Sie könnten dazu Folgendes verwenden:

try
    AssignFile(F, FileName);
    Reset(F);     // raises an EInOutError exception if file is not found
except
    on Exception do ...
end;

Aber Sie könnten mit der folgenden Anweisung den Overhead der Exception-Behandlung vermeiden:

if FileExists(FileName) then    // returns False if file is not found; raises no exception

begin
    AssignFile(F, FileName);
    Reset(F);
end;

Assertions stellen eine weitere Möglichkeit dar, eine boolesche Bedingung im Quelltext zu testen. Wenn eine Assert-Anweisung fehlschlägt, wird entweder das Programm mit einem Laufzeitfehler angehalten oder (wenn die Unit SysUtils verwendet wird) eine SysUtils.EAssertionFailed ausgelöst. Assertions sollten nur zum Testen von Bedingungen verwendet werden, deren Auftreten unwahrscheinlich ist.

Exception-Typen deklarieren

Exception-Typen werden genau wie andere Klassen deklariert. Eigentlich können Sie eine Instanz einer beliebigen Klasse als Exception verwenden. Es ist aber zu empfehlen, Exceptions immer von der Klasse SysUtils.Exception (Unit SysUtils) abzuleiten.

Mithilfe der Vererbung können Exceptions in Familien organisiert werden. Die folgenden Deklarationen in SysUtils definieren beispielsweise eine Familie von Exception-Typen für mathematische Fehler:

type
   EMathError = class(Exception);
   EInvalidOp = class(EMathError);
   EZeroDivide = class(EMathError);
   EOverflow = class(EMathError);
   EUnderflow = class(EMathError);

Aufgrund dieser Deklarationen können Sie eine Behandlungsmethode für SysUtils.EMathError-Exceptions definieren und in dieser auch SysUtils.EInvalidOp, SysUtils.EZeroDivide, SysUtils.Overflow und SysUtils.EUnderflow behandeln.

In Exception-Klassen sind manchmal auch Felder, Methoden oder Eigenschaften definiert, die zusätzliche Informationen über den Fehler liefern. Beispiel:

type EInOutError = class(Exception)
       ErrorCode: Integer;
     end;

Exceptions auslösen und behandeln

Um ein Exception-Objekt auszulösen, verwenden Sie eine Instanz der Exception-Klasse mit einer raise-Anweisung. Beispiel:

raise EMathError.Create;

Im Allgemeinen hat eine raise-Anweisung folgende Form:

raise object at address

Objekt und at Adresse sind optional. Bei Angabe einer Adresse kann es sich um einen beliebigen Ausdruck handeln, der einen Zeigertyp ergibt. In der Regel handelt es sich um einen Zeiger auf eine Prozedur oder eine Funktion. Beispiel:

raise Exception.Create('Missing parameter') at @MyFunction;

Mithilfe dieser Option kann die Exception an einem früheren Punkt im Stack ausgelöst werden.

Wenn eine Exception ausgelöst (d. h. in einer raise-Anweisung angegeben) wird, unterliegt sie einer speziellen Behandlungslogik. Die Programmsteuerung wird durch eine raise-Anweisung nicht auf normale Weise zurückgegeben. Sie wird stattdessen an die innerste Behandlungsroutine übergeben, die Exceptions der jeweiligen Klasse verarbeiten kann. (Bei der innersten Behandlungsroutine handelt es sich um diejenige, deren try...except-Block zuletzt ausgeführt, aber noch nicht beendet wurde.)

In der folgenden Funktion wird ein String in einen Integer-Wert konvertiert. Wenn dieser Wert nicht innerhalb eines bestimmten Bereichs liegt, wird eine SysUtils.ERangeError-Exception ausgelöst.

function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
    Result := StrToInt(S);   // StrToInt is declared in SysUtils
    if (Result < Min) or (Result > Max) then
       raise ERangeError.CreateFmt('%d is not within the valid range of %d..%d', [Result, Min, Max]);
end;

Beachten Sie die Methode CreateFmt, die in der raise-Anweisung aufgerufen wird. SysUtils.Exception und ihre Nachkommen verfügen über spezielle Konstruktoren, um Fehlermeldungen und Kontext-IDs zu erstellen.

Eine ausgelöste Exception wird nach ihrer Behandlung automatisch wieder freigegeben. Versuchen Sie daher niemals, Exceptions manuell freizugeben.

Hinweis: Das Auslösen einer Exception im initialization-Abschnitt einer Unit führt möglicherweise nicht zum gewünschten Ergebnis. Die normale Exception-Unterstützung wird durch die Unit <spanSysUtils eingebunden, die daher zuerst initialisiert werden muss. Wenn während der Initialisierung eine Exception ausgelöst wird, werden alle initialisierten Units (einschließlich <spanSysUtils) finalisiert, und die Exception wird erneut ausgelöst. Anschließend wird sie abgefangen und behandelt. Dabei wird das Programm normalerweise unterbrochen. Ähnlich führt das Auslösen einer Exception im finalization-Abschnitt einer Unit eventuell nicht zum gewünschten Ergebnis, falls bei Auslösen der Exception <spanSysUtils bereits finalisiert war.

Die Anweisung try...except

Exceptions werden in try...except-Anweisungen behandelt. Beispiel:

try
   X := Y/Z;
   except
     on EZeroDivide do HandleZeroDivide;
end;

In dieser Anweisung wird versucht, Y durch Z zu dividieren. Wird dabei eine SysUtils.EZeroDivide-Exception (Division durch null) ausgelöst, wird die Routine HandleZeroDivide aufgerufen.

Die Syntax einer try...except-Anweisung lautet folgendermaßen:

try statements except exceptionBlock end

Anweisungsliste ist eine Folge beliebiger Anweisungen, die durch Semikolons voneinander getrennt sind. exceptionBlock ist entweder

  • eine weitere Anweisungsfolge oder
  • eine Folge von Exception-Behandlungsroutinen, optional mit nachfolgender Klausel
else statements

Eine Exception-Behandlungsroutine hat folgende Form:

on Bezeichner: Typ do Anweisung

Bezeichner: ist optional und kann ein beliebiger Bezeichner sein. Typ ist ein für die Exception verwendeter Typ, und Anweisung ist eine beliebige Anweisung.

In einer try...except-Anweisung werden zuerst die Programmzeilen in der Anweisungsliste ausgeführt. Werden dabei keine Exceptions ausgelöst, wird der Exception-Block (exceptionBlock) ignoriert und die Steuerung an den nächsten Programmteil übergeben.

Wird bei der Ausführung der Anweisungsliste eine Exception ausgelöst (entweder durch eine raise-Anweisung in der Anweisungsliste oder eine von der Anweisungsliste aufgerufene Prozedur bzw. Funktion), versucht das Programm, diese zu behandeln:

  • Stimmen Behandlungsroutinen im Exception-Block mit der betreffenden Exception überein, wird die Steuerung an die erste dieser Routinen übergeben. Eine Übereinstimmung liegt vor, wenn der Typ in der Behandlungsroutine der Klasse der Exception oder eines ihrer Nachkommen entspricht.
  • Wenn keine Behandlungsroutine vorhanden ist, wird die Steuerung an die Anweisung in der else-Klausel übergeben (falls vorhanden).
  • Besteht der Exception-Block lediglich aus einer Folge von Anweisungen (ohne Exception-Behandlungsroutinen), wird die Steuerung an die erste Anweisung in der Liste übergeben.

Trifft keine dieser Bedingungen zu, wird die Suche im Exception-Block der zuletzt ausgeführten und noch nicht beendeten try...except-Anweisung fortgesetzt. Kann dort keine entsprechende Behandlungsroutine, else-Klausel oder Anweisungsliste gefunden werden, wird die nächste try...except-Anweisung durchsucht usw. Wurde die Exception bei Erreichen der äußersten try...except-Anweisung immer noch nicht behandelt, wird das Programm beendet.

Beim Behandeln einer Exception wird der Aufruf-Stack nach oben bis zu der Prozedur oder Funktion durchlaufen, in der sich die try...except-Anweisung befindet, in der die Behandlung durchgeführt wird. Die Steuerung wird dann an die entsprechende Exception-Behandlungsroutine, else-Klausel oder Anweisungsliste übergeben. Bei diesem Vorgang werden alle Prozedur- und Funktionsaufrufe verworfen, die nach dem Eintritt in den try...except-Block stattgefunden haben. Anschließend wird das Exception-Objekt durch einen Aufruf seines Destruktors Destroy automatisch freigegeben, und die Programmausführung wird mit der nächsten Anweisung nach der try...except-Anweisung fortgesetzt. (Das Exception-Objekt wird auch automatisch freigegeben, wenn die Behandlungsroutine durch einen Aufruf der Standardprozedur Exit, Break oder Continue verlassen wird.)

Im folgenden Beispiel sind drei Behandlungsroutinen definiert. Die erste behandelt Divisionen durch null, die zweite Überläufe, und die dritte alle anderen mathematischen Exceptions. SysUtils.EMathError ist zuletzt aufgeführt, da es der Vorfahr der anderen beiden Exception-Klassen ist. Würde es an erster Stelle genannt, käme es nie zu einem Aufruf der beiden anderen Routinen:

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
end;

Vor dem Namen der Exception-Klasse kann optional ein Bezeichner angegeben werden. Dieser Bezeichner dient in der auf on...do folgenden Anweisung zum Zugriff auf das Exception-Objekt. Der Gültigkeitsbereich des Bezeichners ist auf diese Anweisung beschränkt. Beispiel:

try
  ...
except
  on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;

Im Exception-Block kann auch eine else-Klausel angegeben werden. In der else-Klausel werden Exceptions behandelt, die nicht von den Behandlungsroutinen für den Block abgedeckt werden. Beispiel:

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
else
  HandleAllOthers;
end;

In diesem Fall werden in der else-Klausel alle Exceptions außer SysUtils.EMathError behandelt.

Ein Exception-Block, der nur eine Liste von Anweisungen, jedoch keine Behandlungsroutinen enthält, behandelt alle Exceptions. Beispiel:

try
   ...
except
   HandleException;
end;

Hier behandelt die Routine HandleException alle Exceptions, die bei der Ausführung der Anweisungen zwischen try und except ausgelöst werden.

Exceptions erneut auslösen

Wenn das reservierte Wort raise in einem Exception-Block vorkommt:

  • Verwenden Sie raise ohne Zusatz, um das aktuelle Exception-Objekt erneut auszulösen. Auf diese Weise kann in einer Behandlungsroutine begrenzt auf einen Fehler reagiert und anschließend die Exception erneut ausgelöst werden. Diese Möglichkeit ist hilfreich, wenn in einer Prozedur oder Funktion nach Auftreten einer Exception Bereinigungen durchgeführt werden sollen. Beispiel:
 try
   raise Exception.Create('Error Message');
 except
   on E: Exception do
   begin
     E.Message := E.Message +sLineBreak+ 'FATAL';
     raise;
   end;
 end;
  • Lösen Sie ein aktuelles Exception-Objekt NICHT mit raise E aus, verwenden Sie stattdessen nur raise. Ansonsten ist eine Zugriffsverletzung möglich. Das ist falsch:
 try
   raise Exception.Create('Error Message');
 except
   on E: Exception do
   begin
     E.Message := E.Message +sLineBreak+ 'FATAL';
     raise E;
   end;
 end;

Im folgenden Beispiel wird von der Funktion GetFileList ein TStringList-Objekt zugewiesen und mit den Namen der Dateien im übergebenen Pfad gefüllt:

function GetFileList(const Path: string): TStringList;
var
  I: Integer;
  SearchRec: TSearchRec;
begin
  Result := TStringList.Create;
  try
    I := FindFirst(Path, 0, SearchRec);
    while I = 0 do
      begin
          Result.Add(SearchRec.Name);
          I := FindNext(SearchRec);
      end;
  except
      Result.Free;
      raise;
  end;
end;

GetFileList erstellt ein TStringList-Objekt und mithilfe der Funktionen FindFirst und FindNext (definiert in SysUtils) initialisiert. Wenn die Initialisierung fehlschlägt (z. B. wegen eines ungültigen Suchpfades oder zu wenig Speicher, um die String-Liste zu füllen), muss GetFileList die neue String-Liste freigeben, da sie der aufrufenden Routine noch nicht bekannt ist. Aus diesem Grund muss die Initialisierung der String-Liste in einer try...except-Anweisung durchgeführt werden. Bei einer Exception wird die String-Liste vom Exception-Block der Anweisung freigegeben und anschließend die Exception erneut ausgelöst.

Verschachtelte Exceptions

In einer Exception-Behandlungsroutine ausgeführter Code kann Exceptions auslösen und behandeln. Solange diese Exceptions ebenfalls innerhalb der Exception-Behandlungsroutine behandelt werden, haben sie keinen Einfluss auf die ursprüngliche Exception. Wenn eine in einer Exception-Behandlungsroutine ausgelöste Exception jedoch die Routine verlässt, geht die ursprüngliche Exception verloren. Dies ist am Beispiel der Tan-Funktion unten dargestellt:

type
   ETrigError = class(EMathError);
   function Tan(X: Extended): Extended;
   begin
      try
        Result := Sin(X) / Cos(X);
      except
        on EMathError do
        raise ETrigError.Create('Invalid argument to Tan');
      end;
   end;

Wenn während der Ausführung von Tan eine SysUtils.EMathError auftritt, löst die Behandlungsroutine eine ETrigError-Exception aus. Da in Tan keine Behandlungsroutine für ETrigError angegeben ist, verlässt die Exception die Behandlungsroutine, und die ursprüngliche SysUtils.EMathError-Exception wird freigegeben. Für die aufrufende Routine stellt sich der Vorgang so dar, als ob die Tan-Funktion eine ETrigError-Exception ausgelöst hat.

Die Anweisung try...finally

In manchen Situationen muss sichergestellt sein, dass bestimmte Operationen auch bei Auftreten einer Exception vollständig abgeschlossen werden. Wenn beispielsweise in einer Routine eine Ressource zugewiesen wird, ist es häufig wichtig, dass sie unabhängig von der Beendigung der Routine freigegeben wird. In diesen Fällen können Sie eine try...finally-Anweisung verwenden.

Das folgende Beispiel zeigt, wie eine Datei auch dann wieder geschlossen werden kann, wenn beim Öffnen oder Bearbeiten eine Exception auftritt:

Reset(F);
try
   ... // process file F
finally
   CloseFile(F);
end;

Eine try...finally-Anweisung hat folgende Syntax:

try statementList1 finally statementList2 end

Jede Anweisungsliste setzt sich aus einer Folge von Anweisungen zusammen, die durch ein Semikolon voneinander getrennt sind. In dem try...finally-Block werden zuerst die Anweisung in Anweisungsliste1 (try-Klausel) ausgeführt. Wird Anweisungsliste1 ohne Auslösen von Exceptions abgeschlossen, wird Anweisungsliste2 (finally-Klausel) ausgeführt. Wird bei der Ausführung der Anweisungsliste1 eine Exception ausgelöst, wird die Steuerung an Anweisungsliste2 übertragen. Wenn die Ausführung von Anweisungsliste2 beendet ist, wird die Exception erneut ausgelöst. Wenn durch einen Aufruf der Prozedur Exit, Break oder Continue Anweisungsliste1 die Steuerung verliert, wird automatisch Anweisungsliste2 ausgeführt. Daher wird die finally-Klausel unabhängig davon, wie die try-Klausel beendet wird, immer ausgeführt.

Wird eine Exception ausgelöst, aber in der finally-Klausel nicht behandelt, wird sie aus der try...finally-Anweisung weitergegeben, und jede bereits in der try-Klausel ausgelöste Exception geht verloren. In der finally-Klausel sollten daher alle lokal ausgelösten Exceptions behandelt werden, damit die Weitergabe anderer Exceptions nicht beeinträchtigt wird.

Exception-Standardklassen und -Standardroutinen

In den Units SysUtils und System sind verschiedene Standardroutinen für die Exception-Behandlung deklariert, z. B. ExceptObject, ExceptAddr und ShowException. SysUtils, System und andere Units enthalten auch zahlreiche Exception-Klassen (außer OutlineError), die von SysUtils.Exception abgeleitet sind.

Die Klasse SysUtils.Exception verfügt über die Eigenschaften Message und HelpContext, durch die eine Fehlerbeschreibung und eine Kontext-ID für die kontextbezogene Online-Dokumentation übergeben werden kann. Außerdem definiert sie verschiedene Konstruktor-Methoden, mit denen Fehlerbeschreibungen und Kontext-IDs auf unterschiedliche Arten angegeben werden können.

Siehe auch