Visual Basic .NET: Fehlerbehandlung

Aus Wikibooks

Früher hatten es die Programmierer nicht leicht. Ständig musste man sichergehen, dass man keinen Fehler macht, denn Fehler hätten an der Hardware schwere Schäden anrichten können oder zumindest den Rechner zum Absturz bringen. Zum Beispiel ist eine Division durch Null für die Funktionalität einer mathematischen Berechnungseinheit nicht besonders förderlich. Auch der Versuch, in eine nicht vorhandene Datei zu schreiben, kann für ein Dateisystem gefährlich werden.

Also musste man alles gegenprüfen. Wollte man zum Beispiel in eine Datei schreiben, musste man prüfen, ob das Laufwerk existiert, verfügbar ist und die Datei existiert. Dann musste man die Datei öffnen und prüfen, ob das erfolgreich war und ob man überhaupt in die Datei schreiben kann. Dann konnte man schreiben, schloss die Datei und musste prüfen, ob die Datei auch richtig geschlossen wurde. Nach Wunsch konnte man auch noch nachprüfen, ob der Schreibvorgang erfolgreich war, indem man die Datei noch mal ausliest. Das schließt natürlich alle vorangegangenen Tests wieder ein.

Sie sehen schon: Das ist uns nichts. Zum Glück haben wir heute die Zukunft. Die Zukunft heißt in diesem Fall „strukturierte Fehlerbehandlung“. Das bedeutet, dass man den Computer anweist, einen bestimmten Code „auszuprobieren“. Wenn ein Fehler auftritt, „wirft“ die Funktion, in dem der Fehler aufgetreten ist, eine „Ausnahme“, den die aufrufende Funktion „auffangen“ kann. Die gewählten Begriffe sind kein Scherz, sondern wortwörtliche Übersetzungen der zu verwendenden Schlüsselwörter. Kernstück der ganzen Sache sind die Anweisungen Try zum Versuch und Throw beim Irrtum.

Versuch...[Bearbeiten]

Der einfachste Fehler, der auftreten kann, ist ein arithmetischer Fehler, zum Beispiel bei einer Division durch Null oder durch einen Überlauf (weil eine Operation eine für den jew. Datentyp zu großen Wert ergibt). Eine zweite sehr häufige Fehlerquelle stellen eingrenzende Konvertierungen dar. Der folgende Code ist anfällig für solche Fehler.

Code:  

Dim a As Byte = InputBox("a = ")
Dim b As Byte = InputBox("b = ")
MessageBox.Show("Produkt: " & a * b)
MessageBox.Show("Quotient: " & a / b)

Ausgabe:  

Bei den Beispieleingaben a = 4 und b = 2:
Produkt: 8
Quotient: 2
Bei den Beispieleingaben a = 200 und b = 200:
Fehler (Überlauf) -> Programmabbruch
Bei den Beispieleingaben a = 4 und b = 0:
Produkt: 0
Quotient: +unendlich
Bei der Beispieleingabe a = hallo:
Fehler (ungültige Konvertierung)

Ich habe hier bewusst den Datentyp Byte gewählt, da dieser Datentyp im wahrsten Sinne des Wortes schnell an seine Grenzen stößt. Oben sieht man das Beispiel „a = 200 und b = 200“. Werden diese Werte multipliziert, kommt 40000 heraus. Das ist aber zu groß für eine Byte-Variable. Der Überlauf wird als Fehlermeldung angezeigt, und damit nichts durcheinander kommt, wird das Programm sicherheitshalber beendet. Es könnte ja sein, dass dieser Wert nochmal von Bedeutung ist, und das ist er ja auch, nämlich für den Aufruf der MessageBox.Show-Funktion. Leider hat das zur Folge, dass weder die Division ausprobiert wird, noch der Benutzer Gelegenheit erhält, seine Eingabe zu korrigieren. Interessanterweise bricht das Beispiel „a = 4 und b = 0“ nicht mit einem Fehler ab, sondern gibt „+unendlich“ aus. Der Computer ist halt bemüht, für jede Rechnung ein Ergebnis zu definieren. Das Beispiel „a = hallo“ bricht wegen einer ungültigen Konvertierung an. Der String "hallo", der von der InputBox-Funktion zurückgegeben wird, kann nicht in Integer umgewandelt werden. Logischerweise können nur eingrenzende Konvertierungen solche Fehler verursachen.

Mit einer Anweisung lassen sich diese ganzen Unannehmlichkeiten verhindern: mit der Try-Anweisung (engl. try = versuchen, ausprobieren). Die Try-Anweisung definiert zunächst den Try-Block. Hier stehen die Befehle, die ausprobiert werden sollen, in diesem Fall die MessageBox.Show-Aufrufe mit den kritischen Berechnungen. Wenn ein Befehl schief geht, gibt dieser an die Try-Anweisung eine Ausnahme zurück. Die Ausnahme (engl. exception) ist ein Objekt und enthält Informationen über den aufgetretenen Fehler. Nach dem Try-Block können beliebig viele Catch-Blöcke folgen. Jeder Catch-Block behandelt eine Art von Ausnahme, etwa den Überlauf. Ein Catch-Block kann aber auch eine ganze Familie von Ausnahmen behandeln, z.B. alle Fehler, die beim Rechnen auftreten, oder allgemein alle Fehler. Mindestens ein Catch-Block ist aber Pflicht, es sei denn, man definiert einen Finally-Block (dazu später mehr).

Der Try-Block wird mit dem einzelnen Schlüsselwort Try eingeleitet. Nach dem Try-Block folgt der Catch-Block, der durch das Schlüsselwort Catch, gefolgt von der Deklaration der Ausnahmenvariable, eingeleitet wird. Mit einer ähnlichen Zeile wird der aktuelle Catch-Block abgeschlossen und ein weiterer Catch-Block eingeleitet. Den letzten Catch-Block schließt End Try ab. Das folgende Beispiel verdeutlicht die ganze Sache.

Code:  

Try
    Dim a As Byte = InputBox("a = ")
    Dim b As Byte = InputBox("b = ")
    MessageBox.Show("Produkt: " & a * b)
    MessageBox.Show("Quotient: " & a / b)
Catch ex As OverflowException
    MessageBox.Show("Die eingegebenen Werte sind zu groß.")
Catch ex As InvalidCastException
    MessageBox.Show("Bitte nur Zahlen eingeben.")
End Try
End

Ausgabe:  

Bei den Beispieleingaben a = 4 und b = 2:
Produkt: 8
Quotient: 2
Bei den Beispieleingaben a = 200 und b = 200:
Die eingegebenen Werte sind zu groß.
Bei den Beispieleingaben a = 4 und b = 0:
Produkt: 0
Quotient: +unendlich
Bei der Beispieleingabe a = hallo:
Bitte nur Zahlen eingeben.

Nun tritt kein unkontrollierter Fehler mehr auf. Stattdessen wird, falls ein Fehler, sprich eine Ausnahme, auftritt, der entsprechende Catch-Block aufgerufen. Doch der Reihe nach: Die erste Ausnahme tritt in den oben gezeigten Beispielen im Falle „a = 200 und b = 200“ auf. Sie heißt OverflowException (engl. overflow = Überlauf, exception = Ausnahme) und steht für die Tatsache, dass das Ergebnis einer Berechnung zu groß für den Zieldatentyp war. Mit einem Catch-Block (engl. catch = fangen) fangen wir diese Ausnahme auf. ex As OverflowException steht dabei dafür, dass die Ausnahme in einer Variable namens ex verfügbar ist. Dadurch kann man Nachrichten aus der Nachricht extrahieren, in unserem Falle dient diese Deklaration nur dazu, alle Ausnahmen zu filtern. Schließlich sollen mit diesem Catch-Block nur OverflowExceptions abgefangen werden. Eine Alternative wäre die einleitende Zeile Catch ex As ArithmeticException. ArithmeticException ist nämlich die Oberart von OverflowException und anderen Ausnahmen, etwa DivisionByZeroException (in manchen Fällen ist die Division durch Null ein Fehler, in unserem Fall jedoch nicht). Ein Block Catch ex As ArithmeticException fängt also OverflowExceptions und DivisionByZeroExceptions ab. Es wird empfohlen, immer ex als Variablennamen für eine Ausnahme zu verwenden und diesen Namen sonst nicht zu benutzen. Die Variable wird nämlich am Ende des Catch-Blockes entladen, weshalb der Name in allen Catch-Blöcken wiederverwendet werden kann.

In unserem Fall besteht die Fehlerbehandlung aus einer eigenen Fehlermeldung. Man beachte, dass das Programm nicht automatisch abgebrochen wird. Allerdings wird es nach der Fehlerbehandlung durch die End-Anweisung beendet. Nach der Ausführung des jeweiligen Catch-Blockes springt die Ausführung nämlich an das Ende der Catch-Anweisung.

Bei der Eingabe eines ungültigen Wertes, z.B. "hallo", wird der Block Catch ex As OverflowException übersprungen, da diese Ausnahme keine OverflowException, sondern eine InvalidCastException ist (engl. invalid cast = ungültige Konvertierung). Diese Ausnahme wird vom zweiten Catch-Block abgedeckt, der ebenfalls eine entsprechende Fehlermeldung ausgibt. Ein Problem hat unser Programm aber noch: Es sagt dem Benutzer zwar, was er falsch gemacht hat, gibt ihm aber keine Gelegenheit, das zu korrigieren. Ein zweiter Entwurf soll das ändern.

Code:  

Dim VorgangAbgeschlossen As Boolean = False
Do
    Try
        Dim a As Byte = InputBox("a = ")
        Dim b As Byte = InputBox("b = ")
        MessageBox.Show("Produkt: " & a * b)
        MessageBox.Show("Quotient: " & a / b)
        VorgangAbgeschlossen = True
    Catch ex As OverflowException
        MessageBox.Show("Die eingegebenen Werte sind zu groß.")
    Catch ex As InvalidCastException
        MessageBox.Show("Bitte nur Zahlen eingeben.")
    End Try
Loop Until VorgangAbgeschlossen
End

Es wird solange versucht, aus eingegebenen Zahlen Produkt und Quotient zu berechnen, bis VorgangAbgeschlossen wahr wird. Das ist genau dann der Fall, wenn bei Produkt- und Quotientberechnung kein Fehler auftritt, denn dann lösen die Zeilen mit MessageBox.Show keine Ausnahme aus und die Ausführung gelangt zu VorgangAbgeschlossen = True.

...und Irrtum[Bearbeiten]

Nun wissen wir, wie man Fehler abfängt. Wie aber werden solche Fehler erzeugt? Diese Möglichkeit ist keineswegs auf die .NET-Bibliothek beschränkt, sondern kann von jedem Programm, jeder Funktion verwendet werden. Die entsprechende Anweisung heißt Throw (engl. werfen). Damit werfen wir die Ausnahme, die von einem Catch-Block aufgefangen und behandelt wird. Man sollte keine Ausnahme unbehandelt lassen, denn eine unbehandelte Ausnahme ist nicht nur ein Zeichen eines unvorsichtigen, wenn nicht gefährlichen, Programmierstils, sondern auch eine unnötige Last für den Benutzer, der dazu vielleicht noch wichtige Daten verliert.

Also immer die Ausnahmen auffangen. Für die Throw-Anweisung muss ein Ausnahmeobjekt erzeugt werden. Das geht mit der New-Anweisung, die eigentlich im Zusammenhang mit der objektorientierten Programmierung steht und deshalb jetzt nur angeschnitten wird. Ein einfaches Beispiel soll die ganze Sache verdeutlichen.

Code:  

Private Function Dividieren(ByVal Wert1 As Double, ByVal Wert2 As Double) As Double
    If Wert2 = 0 Then Throw New DivideByZeroException()
    Return Wert1 / Wert2
End Function

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Try
        MessageBox.Show(Dividieren(4,2))
        MessageBox.Show(Dividieren(4,0))
    Catch ex As DivideByZeroException
        MessageBox.Show("Division durch Null!")
    End Try
    End
End Sub

Ausgabe:  

2
Division durch Null!

Hier haben wir neben der Form1_Load-Funktion eine weitere Funktion namens Dividieren, die zwei Zahlen durcheinander teilt. Dabei wird sichergestellt, dass keine Division durch Null durchgeführt wird. Wenn der zweite Wert dennoch Null ist, wird eine DivideByZeroException ausgelöst. Das geht mit dem Befehl Throw New DivideByZeroException. Der besteht eigentlich aus zwei Anweisungen (Throw und New). Die Anweisung New DivideByZeroException erstellt ein neues DivideByZeroException-Objekt. (Was das soll, soll uns erstmal nicht interessieren, da das mit Objektorientierung zusammenhängt. Nehmen Sie es deshalb erstmal hin, dass mit New und Objektbezeichner ein neues solches Objekt erstellt wird.)

Die New-Anweisung hat einen Rückgabewert, nämlich das neue Objekt. Die Throw-Anweisung erfordert, quasi als Parameter, dieses Objekt, das dann geworfen wird. Anders als bei einem Funktionsaufruf müssen hier keine Klammern um den Parameter gesetzt werden, sodass der Befehl so aussieht: Throw Objekt. Kombiniert mit der New-Anweisung ergibt sich Throw New Objekt, so wie im obigen Beispiel zu sehen ist. Throw hat den gleichen Effekt wie Return, nämlich dass die Ausführung der Funktion beendet wird. Auch wenn ein Catch-Block behandelt wird, wird die Ausführung nicht nach der Throw-Anweisung, sondern nach der Try-Anweisung der aufrufenden Funktion fortgesetzt.

Durch Throw wird kein normaler Rückgabewert zurückgegeben. Man kann sich das so vorstellen, dass die Ausführung des normalen Codes gestoppt wird und nach einem Catch-Block gesucht wird. Wenn in der Funktion, in der die Throw-Anweisung steht, keiner gefunden wird, wird in der aufrufenden Funktion gesucht, dann in der Funktion, die diese aufgerufen hat, und so weiter. Wird keiner gefunden, geht das über die Verwaltungsschicht des .NET-Frameworks bis in die Windows-Ausführungsschicht zurück, die den Prozess beendet und eine Fehlermeldung anzeigt.

Das war jetzt ganz schön theoretisch. Wichtig ist nur soviel: Mit Throw lassen sich Ausnahmen werfen, die von einem Catch-Block in der jeweiligen Funktion oder einer der aufrufenden Funktionen behandelt werden. Bleibt die Ausnahme unbehandelt, wird das Programm abgebrochen.

Finally[Bearbeiten]

Nach den Catch-Blöcken kann man einen weiteren Block einfügen, den Finally-Block (engl. finally = zum Schluss). Hier werden Befehle eingefügt, die nach dem Try-Block auf jeden Fall ausgeführt werden sollen, selbst wenn eine Ausnahme ausgelöst wurde. So können wir zum Beispiel Objekte aufräumen, was in der objektorientierten Programmierung sehr nützlich ist, da zum Beispiel der Zugriff auf eine Datei explizit abgeschlossen werden muss. Die entsprechende Anweisung steht dann im Finally-Block. Ein einfaches Beispiel dazu:

Code:  

Dim VorgangAbgeschlossen As Boolean = False
Do
    Try
        Dim a As Byte = InputBox("a = ")
        Dim b As Byte = InputBox("b = ")
        MessageBox.Show("Produkt: " & a * b)
        MessageBox.Show("Quotient: " & a / b)
        VorgangAbgeschlossen = True
    Catch ex As OverflowException
        MessageBox.Show("Die eingegebenen Werte sind zu groß.")
    Catch ex As InvalidCastException
        MessageBox.Show("Bitte nur Zahlen eingeben.")
    Finally
        MessageBox.Show("Schleifendurchlauf abgeschlossen.")
    End Try
Loop Until VorgangAbgeschlossen
End

Ein Test zeigt, dass nach jedem Durchlauf die Meldung „Schleifendurchlauf abgeschlossen.“ angezeigt wird, selbst wenn eine Ausnahme ausgelöst wurde. Ich verzichte hier auf ein konkretes Beispiel, da der wirkliche Nutzen von Finally so gut wie nur in der OOP liegt. Man kann den Block aber auch nutzen, um Code zu notieren, der am Ende des Try-Blockes und aller Catch-Blöcke ausgeführt worden wäre. Eine interessante Eigenheit des Finally-Blockes ist übrigens, dass er sogar dann ausgeführt wird, wenn im Try-Block oder im Catch-Block eine Exit-Anweisung ausgeführt wurde, zum Beispiel Exit Sub.