Exceptions (Object Pascal)

Aus Appmethod Topics
Wechseln zu: Navigation, Suche

Nach oben zu Klassen und Objekte - Index


In diesem Thema werden folgende Bereiche erläutert:

  • Ü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. Außerdem bietet sie sich für Exceptions an, die vom Betriebssystem oder von Routinen ausgelöst werden, auf deren Quellcode kein Zugriff möglich ist.

Normalerweise kommen Exceptions bei Hardware-, Speicher-, E/A- und Betriebssystemfehlern zum Einsatz. Bedingungsanweisungen sind oft die beste Methode für einen Fehlertest. Ein Beispiel:

try
    AssignFile(F, FileName);
    Reset(F);     // Löst die Exception EInOutError aus, wenn die Datei nicht gefunden wird
except
    on Exception do ...
end;

Mit der folgenden Anweisung können Sie den Aufwand der Exception-Behandlung vermeiden:

if FileExists(FileName) then    // Gibt False zurück, wenn die Datei nicht gefunden wird; löst keine Exception aus
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 (bei Verwendung der Unit SysUtils) eine SysUtils.EAssertionFailed-Exception 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 SysUtils.Exception-Klasse (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 bereitstellen und in dieser auch SysUtils.EInvalidOp, SysUtils.EZeroDivide, SysUtils.EOverflow 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 Objekt at Adresse

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('Fehlender 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 dieser handelt es sich um die Routine, 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 ist in SysUtils deklariert
    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. Die SysUtils.Exception-Klasse 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 nicht zum gewünschten Ergebnis. Die normale Exception-Unterstützung wird durch die Unit SysUtils eingebunden, die daher zuerst initialisiert werden muss. Wenn während der Initialisierung eine Exception ausgelöst wird, werden alle initialisierten Units (einschließlich SysUtils) 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 SysUtils bereits abgeschlossen wurde.

Die Anweisung try...except

Exceptions werden mithilfe von try...except-Anweisungen behandelt. Beispiel:

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

Zuerst wird im try-Block die Division Y / Z durchgeführt. Tritt dabei eine SysUtils.EZeroDivide-Exception (Division durch Null) auf, wird die Behandlungsroutine HandleZeroDivide aufgerufen.

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

try Anweisungsliste except ExceptionBlock end

Anweisungsliste ist eine Folge beliebiger Anweisungen, die durch einen Strichpunkt voneinander getrennt sind. ExceptionBlock ist entweder

  • eine weitere Anweisungsfolge oder
  • eine Folge von Exception-Behandlungsroutinen, optional mit nachfolgendem
else Anweisungsliste

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 Anweisungsliste ausgeführt. Werden dabei keine Exceptions ausgelöst, wird ExceptionBlock ignoriert und die Steuerung an den nächsten Programmteil übergeben.

Tritt bei der Ausführung der Anweisungsliste eine Exception auf (entweder durch eine raise-Anweisung oder eine aufgerufene Prozedur bzw. Funktion), versucht das Programm, diese zu behandeln:

  • Stimmt eine der Behandlungsroutinen im Exception-Block mit der betreffenden Exception überein, wird die Steuerung an diese Routine übergeben. Eine Übereinstimmung liegt vor, wenn der Typ in der Behandlungsroutine der Klasse der Exception oder eines ihrer Nachkommen entspricht.
  • Wenn keine Behandlungsroutine existiert, 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. Ist die Exception bei Erreichen des äußersten try...except-Blocks immer noch nicht behandelt worden, 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 dem try...except-Block fortgesetzt. Das 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. Der SysUtils.EMathError-Typ ist zuletzt aufgeführt, da er der Vorfahr der anderen beiden Exception-Klassen ist. Würde er 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. Dort werden alle 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 Sie das reservierte Wort raise ohne nachfolgende Objektreferenz in einem Exception-Block angeben, wird die aktuell behandelte Exception nochmals ausgelöst. 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 Aufräumarbeiten durchgeführt werden sollen (z. B. Objekte oder Ressourcen freigeben).

Im folgenden Beispiel wird von der Funktion GetFileList ein TStringList-Objekt erstellt 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;

In dieser Funktion wird ein TStringList-Objekt erstellt und mithilfe der Funktionen FindFirst und FindNext (Unit SysUtils) mit Werten gefüllt. Tritt dabei ein Fehler auf (z. B. aufgrund eines ungültigen Pfades oder wegen Speichermangels), muss das neue Objekt freigegeben werden, da es 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 das Objekt im Exception-Block freigegeben und anschließend die Exception erneut ausgelöst.

Verschachtelte Exceptions

In einer Exception-Behandlungsroutine können wiederum Exceptions ausgelöst und behandelt werden. Solange dieser Vorgang ebenfalls innerhalb der Routine stattfindet, hat er keinen Einfluss auf die ursprüngliche Exception. Wenn die zweite Exception jedoch die Routine verlässt, geht die Original-Exception verloren. Ein Beispiel:

type
   ETrigError = class(EMathError);
   function Tan(X: Extended): Extended;
   begin
      try
        Result := Sin(X) / Cos(X);
      except
        on EMathError do
        raise ETrigError.Create('Ungültiges Argument für Tan');
      end;
   end;

Wenn während der Ausführung von Tan eine SysUtils.EMathError-Exception auftritt, wird in der Behandlungsroutine eine ETrigError-Exception ausgelöst. Da in Tan keine Routine für ETrigError definiert 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 Funktion Tan 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 sehr wichtig, dass sie unabhängig von der Beendigung der Routine wieder freigegeben wird. In diesen Fällen können try...finally-Anweisungen verwendet werden.

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

Reset(F);
try
   ... // Datei F verarbeiten
finally
   CloseFile(F);
end;

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

try Anweisungsliste1 finally Anweisungsliste2 end

Jede Anweisungsliste setzt sich aus einer Folge von Anweisungen zusammen, die durch einen Strichpunkt voneinander getrennt sind. In einem try...finally-Block werden zuerst die Programmzeilen in Anweisungsliste1 (try-Klausel) ausgeführt. Wenn dabei keine Exceptions auftreten, wird anschließend Anweisungsliste2 (finally-Klausel) ausgeführt. Bei einer Exception wird die Steuerung an Anweisungsliste2 übergeben und danach die Exception erneut ausgelöst. Befindet sich ein Aufruf der Standardprozedur Exit, Break oder Continue in Anweisungsliste1, wird dadurch automatisch Anweisungsliste2 aufgerufen. Daher wird die finally-Klausel unabhängig davon, wie der try-Block beendet wird, immer ausgeführt.

Wenn eine Exception ausgelöst, aber in der finally-Klausel nicht behandelt wird, führt sie aus der try...finally-Anweisung hinaus, und jede zuvor in der try-Klausel ausgelöste Exception geht verloren. In der finally-Klausel sollten daher alle lokal ausgelösten Exceptions behandelt werden, damit die Behandlung anderer Exceptions nicht gestört 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 SysUtils.Exception-Klasse 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