Programmierkurs: Delphi: Pascal: Exceptions

Exceptions Bearbeiten

Exceptions (deutsch: Ausnahmen) sind Fehler, die während der Ausführung eines Programms auftreten können.

Exceptions werden immer dann ausgelöst, wenn es erst während des laufenden Programms zu unerlaubten Aufrufen kommt, die während der Kompilierung noch nicht zu erkennen waren. Meist werden dabei falsche oder ungültige Daten an Routinen weitergegeben oder in Berechnungen eingebunden. Die häufigsten Fehler sind dabei Divisionen durch Null oder das Verwenden noch nicht initialisierter Zeiger oder Klassen. Auch der Versuch, eine nicht vorhandene Datei zu laden, gehört hierzu.

Bei all solchen Laufzeitfehlern wird eine Exception ausgelöst, die Sie in Ihrem Programm abfangen und weiter behandeln können.

Exceptions sind spezielle Klassen, die sich alle von der Klasse Exception (Unit SysUtils) ableiten. Alle Exceptions beginnen mit E, viele sind bereits in Delphi vordefiniert. Die bekanntesten Exception-Typen sind wahrscheinlich EMathError, EConvertError und EDivByZero bzw. EZeroDivide. Vor allem die letzten beiden treten häufig auf, wenn man den Divisor nicht prüft. EDivByZero wird ausgelöst, wenn versucht wird, eine Ganzzahl durch Null zu teilen, EZeroDivide wird bei Gleitkommazahlen ausgelöst. EDivByZero tritt z.B. hier auf:

var
  a, b: Integer;
begin
  a := 2;
  b := 0;
  Writeln(a div b);
end;

Ein weiteres Beispiel ist der EConvertError, der zum Beispiel auftritt, wenn man StrToInt eine ungültige Zeichenkette übergibt, also außer einer umwandelbaren Zahl auch Buchstaben oder andere Zeichen enthalten waren.

Abfangen von Exceptions Bearbeiten

In der Delphi-IDE kann man zwar Exceptions abschalten, das unterdrückt jedoch nur die Fehlermeldung, die der Delphi-Debugger anzeigt. Das grundlegende Problem ist immer noch vorhanden und kann unter Umständen den weiteren Programmablauf instabil machen. Besser ist es, angemessen auf solche Ausnahmefehler zu reagieren und anschließend seine Routine oder Methode sauber (und gegebenenfalls mit einem entsprechenden Rückgabewert) zu verlassen.

Delphi bietet hierfür zwei Möglichkeiten an. Der try...except-Block dient dazu, auf verschiedene Exceptions gezielt zu reagieren, während der try...finally-Block auf alle Exceptions gleichermaßen reagiert und meist für Aufräumarbeiten verwendet wird.

Damit eine Exception ausgelöst und unter diesen beiden Blöcken verarbeitet werden kann, müssen Sie die Unit SysUtils in Ihr Programm einbinden. Diese enthält die Basisklasse Exception und stellt damit die grundlegende Möglichkeit der Fehlerbehandlung zur Verfügung.

Mit try...except lässt sich, je nach Fehler, z.B. eine genaue Fehlermeldung ausgeben. Der Aufbau des Blocks ist wie folgt:

try
  <unsichere Anweisungen>
except
  on <Exception> do
    <Anweisung>
  [on <Exception> do
    <Anweisung>]
  ...
else
  <Anweisungen>
end;

Es wird versucht, alle Anweisungen unter try auszuführen. Wenn dabei eine Exception ausgelöst wird, springt das Programm in den except-Block und führt dort die Anweisung für die entsprechende Exception aus. Falls die ausgelöste Exception dort nicht genannt ist, werden die Anweisungen unter else aufgerufen. Der else-Block muss nicht angegeben werden, dient aber dazu, ganz auf Nummer sicher zu gehen. Nach der Behandlung des Fehlers wird das Programm normal fortgesetzt.

Bei dem folgenden Beispiel werden falsche Benutzereingaben verarbeitet:

var
  a, b, c: Byte;

begin
  Write('Wert fuer a? '); Readln(a);
  Write('Wert fuer b? '); Readln(b);
  try
    c := a * b;    // ERangeError, wenn das Ergebnis größer 255 ist
    Writeln(a, ' * ', b, ' = ', c);
    c := a div b;  // EDivByZero, wenn b = 0 ist
    Writeln(a, ' / ', b, ' = ', c);
  except
    on EDivByZero do
      Writeln('Fehler: Division durch 0!');
    on ERangeError do
      Writeln('Fehler: Ergebnis ausserhalb des gueltigen Bereichs!');
  else
    Writeln('Unbekannter Fehler!');
  end;
end.

Der Benutzer wird nun über seine falsche Eingabe gezielt informiert.

Die andere Möglichkeit, der try...finally-Block ist ähnlich aufgebaut:

try
  <unsichere Anweisungen>
finally
  <Anweisungen>
end;

Wie Sie sehen, ist dieser Block weniger umfangreich, da ja bei allen Exceptions die gleichen Anweisungen ausgeführt werden. Unter finally sollten Sie Aufräumarbeiten durchführen, um den Zustand vor Eintritt in den try...finally-Block wieder herzustellen. Z.B. haben Sie ein neues Objekt erstellt, das Sie an eine Anweisung übergeben wollten, wobei der Fehler auftrat. Dann geben Sie den Speicher des Objektes wieder im finally-Block frei.

Bei try...finally ist zu beachten, dass die Anweisungen unter finally in jedem Falle ausgeführt werden, egal ob eine Exception auftritt, ob der try-Block erfolgreich oder mittels Break oder Exit verlassen wird. Weiterhin müssen Sie wissen, dass die Exception am Ende des finally-Blocks erneut ausgelöst wird. Wenn Sie also einen try...finally-Block in einer Methode verwenden, müssen Sie den aufrufenden Programmteil in einer try...except-Klausel einfassen, damit diese Exception dort abschließend bearbeitet werden kann. Geschieht das nicht, wird das Programm am Ende des try...finally-Blocks beendet. Alternativ können Sie auch die Blöcke ineinander verschachteln, die äußerste Fehlerbehandlung sollte dabei immer durch try...except erfolgen, damit das Programm anschließend weiterlaufen kann.

Deklaration Bearbeiten

Wie eingangs schon erwähnt, handelt es sich bei Exceptions um Klassen. Sie können daher eigene Exceptions erstellen, indem Sie Nachkommen von anderen Exceptions deklarieren und dort Anpassungen, wie zum Beispiel die Fehlermeldung, vornehmen.

type
  EMyError = class(Exception)
    constructor Create; overload;
  end;

constructor EMyError.Create;
begin
  inherited Create('Meine eigene Fehlermeldung');
end;

Die Hierarchie würde nun so aussehen:

Exception
   │
   └─── EMyError

Auslösen von Exceptions Bearbeiten

Um an einer Programmstelle eine Exception auszulösen, verwenden Sie die Anweisung raise zusammen mit einem Exception-Objekt:

raise EMyError.Create;

Sie können auch innerhalb einer Ereignisbehandlungsroutine (also im except- oder finally-Block) die aktuell behandelte Exception erneut auslösen. Dazu verwenden Sie raise ohne weitere Angaben.

Exceptions sollten immer dann ausgelöst werden, wenn der weitere Programmablauf durch einen festgestellten Fehler nachhaltig gestört werden würde. Bei der oben genannten Formel a/b erwartet man ein Ergebnis, mit dem man weiterarbeiten kann. Da im Falle von b=0 jedoch kein Ergebnis möglich ist, müssen hier weitere Aktionen angestoßen werden, um den Programmablauf wieder zu korrigieren.

Vor- und Nachteile Bearbeiten

Exceptions und die dazu gehörige Ereignisbehandlung bieten eine einfache Möglichkeit, schwer aufzufindende oder seltene Fehler im Programm festzustellen und zu beheben.

Weiterhin kann man mit Exceptions schwierige und umfangreiche – und damit meist auch unübersichtliche – Vorabprüfungen von Bedingungen vermeiden. Man übergibt also seine Daten ungeprüft an eine Anweisung und kümmert sich erst dann um einen Fehler, falls tatsächlich einer auftritt. Nehmen wir einmal an, Sie haben eine Anweisung, die einen Dateinamen entgegen nimmt, aus dieser Datei Werte ausliest und mit ihnen rechnet. Sie müssten nun vorher prüfen, ob die Datei vorhanden ist, ob sie nicht leer ist und ob die Daten im richtigen Format vorliegen, bevor Sie die Anweisung aufrufen. Dies wäre sehr aufwändig und würde Ihren Programmtext unübersichtlich machen. Hier ist es sinnvoller, die Anweisung einfach „machen zu lassen“ und auf eventuelle Fehler anschließend und gezielt zu reagieren.

Ein weiterer Vorteil von Exceptions ist, dass diese automatisch über mehrere Anweisungsebenen, bis zurück in den Hauptanweisungsblock eines Programms hinaufgereicht werden können. Mit anderen Worten: Sie können selbst bestimmen, an welcher Stelle in Ihrer Anweisungskette welche Exception behandelt wird. Sie haben z.B. eine Routine B, in welcher die Exceptions E1 und E2 auftreten können. Aus dem Hauptprogramm rufen Sie nun eine Routine A auf, die wiederum die Routine B ausführt. In diesem Fall können Sie beispielsweise beide Exceptions im Hauptprogramm oder in A behandeln oder auch E1 in A und E2 erst im Hauptprogramm.

Wenn Sie jedoch eine Exception-Behandlung vermeiden können, tun Sie es bitte grundsätzlich auch. Zum Einen kann der Programmcode gerade bei verschachtelten Behandlungsblöcken schnell unübersichtlich werden. Zum Anderen verbraucht die Ereignisbehandlung natürlich auch kostbare Rechenzeit. Dies passiert beispielsweise, wenn Sie unnötig Objekte erstellen, Dateien laden und eventuell diverse Berechnungsschleifen durchführen, obwohl bereits von vornherein klar war, dass ein übergebener Wert zu einem Fehler führen würde. Auch hierzu möchten wir Ihnen ein kleines Beispiel geben: Sie haben eine Gästeliste von 20 Gästen, für die Sie mithilfe einer Schleife Tischkärtchen ausdrucken möchten. Der Benutzer darf angeben, bis zu welcher Gastnummer der Ausdruck erfolgen soll. Der Benutzer vertippt sich und gibt 22 statt 11 ein. Die Exception bei Gast 21 ist das geringere Problem: Bis sie (endlich) ausgelöst wird, werden neun Seiten zuviel ausgedruckt.

Tipp: Verwenden Sie Exception-Behandlungen nur, wenn Sie Fehler in unterschiedlichen Ebenen einer Anweisungskette abfangen wollen oder wenn eine vorherige Prüfung zu umfangreich bzw. unmöglich ist. Bei einfachen Vorabprüfungen bevorzugen Sie bitte diese. Welche Variante Ihnen besser gefällt, bleibt aber letztlich Ihnen überlassen.


  Pascal: Vererbung Inhaltsverzeichnis Einstieg