Home » Tutorials » Programmierkonzepte » Fehlermeldungen

Fehlermeldungen

Exceptions und deren Verursacher

Schauen wir uns nun einmal ein paar Beispiele für Exceptions an.

Mit Zahlen jonglieren

Folgender Code:

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
begin
  i := 27 div StrToInt(Edit1.Text);
  ShowMessage(IntToStr(i));
end;

Es wird eine Ganzzahldivision durchgeführt und das Ergebnis angezeigt. Warum kann das aber zum Problem werden? Nun angenommen der User gibt im Editfeld eine 0 oder Buchstaben oder einfach gar nichts ein. Was ist die Folge? Richtig: Eine Exception:

Im Projekt Project1.exe ist eine Exception der Klasse EDivByZero mit der Meldung 'Division durch Null' aufgetreten.

bzw.

Im Projekt Project1.exe ist eine Exception der Klasse EConvertError mit der Meldung ''a' ist kein gültiger Integerwert' aufgetreten.

Die Fehlermeldung sollte eigentlich klar sein.
Um so etwas zu vermeiden müssen wir diese Ausnahmefälle abfangen:

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
  divisor: Integer;
begin
  divisor := StrToIntDef(Edit1.Text, 0);
  if divisor <> 0 then
  begin
    i := 27 div divisor;
    ShowMessage(IntToStr(i));
  end
  else
  begin
    ShowMessage('Ein sinnvoller Wert erhöht die Aussicht auf Erfolg drastisch!');
  end;
end;

StrToIntDef wandelt den String in einen Integer-Wert um. Ist der String kein gültiger Integer-Wert, wird ein default-Wert(hier 0) verwendet. So kann dieser Fall einfach abgefangen werden. Eine weitere Möglichkeit ist das Verwenden der Funktion Val(). Das sieht dann folgendermaßen aus:

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
  divisor: Integer;
  ErrorCode: Integer;
begin
  Val(Edit1.Text, divisor, ErrorCode);
  if ErrorCode = 0 then // kein Fehler ==> gültiger Wert
  begin
    i := 27 div divisor;
    ShowMessage(IntToStr(i));
  end
  else
  begin
    ShowMessage('Ein sinnvoller Wert erhöht die Aussicht auf Erfolg drastisch!');
  end;
end;

Val weist im Erfolgsfall der Variablen ErrorCode den Wert 0 zu. Tritt ein Konvertierungsfehler auf, erhält ErrorCode die Fehlerstelle als Wert.
Am besten ist es natürlich, wenn gar nicht erst zugelassen wird, dass ein ungültiger Wert eingegeben wird. Am einfachsten ist es in diesem Fall die Komponente SpinEdit zu verwenden und den Wertebereich im OnChange-Ereignis einzuschränken:

procedure TForm1.SpinEdit1Change(Sender: TObject);
begin
  if SpinEdit1.Value = 0 then
    SpinEdit1.Value := 1;
end;

So ist es gar nicht erst möglich einen falschen Wert einzugeben.
Steht die Spin-Edit-Komponente nicht zur Verfügung oder will man aus anderen Gründen auf sie verzichten, bietet es sich an, das Editfeld bei der Eingabe zu überprüfen und ggf. bestimmte Zeichen nicht zuzulassen:

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  if not (Key in ['0'..'9', #8]) then  // #8 ist Backspace und muss angegeben werden, damit die Eingabe auch gelöscht werden kann
  begin
    Beep; // Signalton ausgeben
    Key := #0; // Eingabe verwerfen
  end;
end;

Hier wird geprüft, ob die Eingabe eine Ziffer ist und nur dann wird die Eingabe zugelassen. Dies schafft uns das Problem mit den Buchstaben vom Hals, allerdings kann der User immer noch eine 0 oder gar nichts eingeben. Außerdem können durch Copy’n’Paste immer noch falsche Werte ins Edit-Feld gelangen. Eine Kombination der genannten Absicherungen ist also sinnvoll…
Dass genau dieselben Fehler auch bei Gleitkommazahlen(Floats) vorkommen können, brauche ich wohl nicht zu sagen. Bis auf die Möglichkeit des Defaultwertes und der SpinEdit-Komponente gibt es kaum Unterschiede zu der Behandlung von Fehlern mit ganzen Zahlen.

Bis an die Grenzen und noch weiter…

Auch bei Arrays(und allem anderen, was sich verhält, wie ein Array, wie z.B. die Lines-Eigenschaft von Memos oder auch Strings) können Exceptions auftreten. Der wohl häufigste heißt:

Im Projekt Project1.exe ist eine Exception der Klasse EStringListError mit der Meldung 'Listenindex überschreitet das Maximum (5)' aufgetreten.

Diese und ähnliche Fehler weisen darauf hin, dass man auf einen nicht vorhandenen Listeneintrag zugreifen will. Sind in einer Listbox z.B. nur 3 Einträge und man will auf den 4. zugreifen, dann ist man gerade dabei, Mist zu bauen. Sowas ist – insbesondere bei Programmieranfängern – schnell passiert. Insbesondere die Tatsache, dass Programmierer die komische Angewohnheit haben mit der 0 anzufangen zu zählen, ist für manchen etwas ungewohnt. So hat der erste Eintrag einer Listbox den Index 0 und der letzte Count -1(!). Bei Strings wiederum ist es wieder anders. Das erste Zeichen eines Strings hat – historisch bedingt – den Index 1 und somit das letzte den Index Length(string). Bei so gut wie allem anderen ist aber die 0 die erste Zahl…

Zugriffsverletzungen

Kommen wir nun zu meinen Lieblings-Exceptions: Den Zugriffsverletzungen bzw. engl. AccessViolations(AVs). Früher dachte ich mal wenn Delphi nix anderes einfällt kommt diese nichtssagende Exception und treibt den Programmierer damit in den Wahnsinn… *grins*
Hierbei handelt es sich um einen Zugriff auf einen Speicherbereich auf den man nicht zugreifen darf.
Sehen wir uns einmal die Meldung genauer an, so erhalten wir mehrere Informationen:

Im Projekt Project1.exe ist eine Exception der Klasse EAccessViolation mit der Meldung 'Zugriffsverletzung bei Adresse 00000005. Lesen von Adresse 00000005' aufgetreten.

Fehlerklasse, Adresse und Vorgang.
Fehlerklasse ist soweit schon mal klar. Zugriffsverletzung. Jo.
Vorgang wird schon schwieriger: in diesem Fall: Lesen. Man kann also auf unterschiedliche Art den Zugriff verletzen: lesend und schreibend. Diese Information sagt uns also etwas über die Art der Zugriffsverletzung, sozusagen die „Mordwaffe“(Dolch oder Pistole, Lesen oder Schreiben).
Kommen wir nun zu Adresse (*shock*): Hier wird (als Hex-Code) die Speicheradresse angegeben, auf die unberechtigterweise zugegriffen wurde. Das entspräche dann so ungefähr der Stelle der Verwundung: Kopf, Herz…
Um einen solchen Fehler nun zu beheben, müssen wir etwas Kriminalkommissar spielen: Wir kennen die Leiche, die Mordwaffe und Art und Stelle der Verletzung. Nur wo steckt der Täter? Das ist manchmal gar nicht so einfach zu ermitteln, denn eine AV kann viele verschiedene Ursachen haben und diese können auch noch lange zurückliegen. Hier mal eine kurze – natürlich unvollständige – Liste der möglichen Ursachen:

  • Bereichsüberschreitung: Je nachdem um welche Art von „Array“ es sich handelt, kann ein Zugriff auf ein nicht existierendes Element statt zu einer „Index überschreitet das Maximum“ oder einer ähnlichen Meldung auch zu einer AccessViolation führen.
  • Zugriff auf noch nicht oder nicht mehr vorhandene Komponenten
  • Zugriff auf einen nil-Pointer
  • Zugriff auf einen Pointer, der irgendwo ins Nirwana zeigt

Manchmal führen diese Aktionen auch noch nicht direkt zur AV. Man schreibt dann einfach mal so im Speicher rum. Wenn der adressierte Speicherbereich weder zum Code-Teil gehört, noch zu dem Bereich, der noch nicht zur Anwendung zugewiesen wurde, kann man – vorerst – ohne Probleme darin herumschreiben. Diese Speicherbereiche enthalten dann entweder Programmdaten oder hatten mal Programmdaten enthalten und liegen nun brach. So. Ab jetzt wird es so n bisschen wie russisch Roulette: War letzteres der Fall, hat man Glück gehabt(bzw. Pech, weil der Fehler ausnahmsweise nicht aufgetreten ist und man deshalb gar nicht merkt, dass man Mist baut). Ansonsten verändert man willkürlich irgendwelche Programmdaten, was dann zu späteren Folgefehlern führen kann. Oft kommt es dann später zu extrem schwer zu reproduzierenden AVs, die vielleicht nur sporadisch auftreten und keinen erkennbaren Muster folgen. Und spätestens dann hat man das Vergnügen ein Gespenst jagen zu dürfen…
Ein Pauschalrezept zum Lösen des oben genanntes Mordfalles gibt es nicht. Sporadische Fehler sind zum Glück relativ selten und so hält sich der Aufwand zum Finden des Fehlers meist in Grenzen. Einige Tipps seinen an dieser Stelle genannt:

  • Ist die Adresse 00000000, so handelt es sich um einen nil-Pointer. Über den Debugger ist der meist leicht auszumachen.
  • Greift man auf noch nicht erstellte oder nicht mehr vorhandene Komponenten(oder allgemein Objekte) zu, so lässt sich auch diese meist durch den Debugger relativ schnell finden.
  • Bereichsüberschreitungen kann man a) ebenfalls per Debugger finden und b) sieht man sie meist schon recht schnell im Quellcode: Irgendwann hat man schon kapiert, dass es immer count -1 heißen muss…