Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 <<   zurück
Visual Basic 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual Basic 2005

Visual Basic 2005
1.233 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-585-1
gp Kapitel 9 Fehlerbehandlung und Debugging
  gp 9.1 Die Behandlung von Laufzeitfehlern
    gp 9.1.1 Laufzeitfehler erkennen
    gp 9.1.2 Die Behandlung von Exceptions
    gp 9.1.3 Die »Try ... Catch«-Anweisung
    gp 9.1.4 Behandlung mehrerer Exceptions
    gp 9.1.5 Die »Finally«-Anweisung
    gp 9.1.6 Das Weiterleiten von Ausnahmen
    gp 9.1.7 Die Hierarchie der Exceptions
    gp 9.1.8 Die Reihenfolge der »Catch«-Klauseln
    gp 9.1.9 Die Basisklasse »Exception«
    gp 9.1.10 Benutzerdefinierte Exceptions
  gp 9.2 Debuggen mit Programmcode
    gp 9.2.1 Einführung
    gp 9.2.2 Die Klasse »Debug«
    gp 9.2.3 Die Klasse »Trace«
    gp 9.2.4 Ablaufverfolgung mit »TraceListener«-Objekten
    gp 9.2.5 Steuerung der Protokollierung mit Schaltern
    gp 9.2.6 Bedingte Kompilierung
  gp 9.3 Debuggen mit dem Visual Studio 2005
    gp 9.3.1 Debuggen im Haltemodus
    gp 9.3.2 Das »Befehlsfenster«
    gp 9.3.3 Weitere Alternativen, um Variableninhalte zu prüfen
  gp 9.4 Das Objekttestcenter (Object Test Bench – OTB)

Kapitel 9 Fehlerbehandlung und Debugging


Galileo Computing

9.1 Die Behandlung von Laufzeitfehlern  downtop

Fast alle Beispiele dieses Buches waren bisher so angelegt, als könnte nie ein Fehler auftreten. Aber Ihnen ist es beim Testen des Beispielcodes sicherlich schon passiert, dass Sie statt einer Zahl einen Buchstaben eingegeben haben oder umgekehrt – genau entgegengesetzt zu dem, was das Programm in diesem Moment erwartete. Sie wurden danach mit einem Laufzeitfehler konfrontiert, was zur sofortigen Beendigung des Programms führte.

Dieser Umstand ist natürlich insbesondere dann unangenehm, wenn bei einem Endanwender ein solcher Fehler auftritt. Sollten diesem dann noch Daten unwiederbringlich verloren gegangen sein, die er sich mühevoll und akribisch erarbeitet hat, ist der Ärger vorprogrammiert. Sie haben einen unzufriedenen Kunden, der an Ihren Qualitäten als Entwickler zweifelt, und anschließend noch die undankbare Aufgabe, den oder gar die Fehler zu lokalisieren und in Zukunft auszuschließen.

Fehler können sehr hässlich sein, insbesondere dann, wenn Nebeneffekte auftreten, die sich vorher nahezu nicht voraussehen lassen. Welcher Entwickler kann zuverlässig voraussehen, welche Eingabe und vielleicht gar noch in welcher Reihenfolge ein Anwender diese tätigt, wenn er die grafische Benutzeroberfläche einer Applikation bedient? Welcher Anwender kann nach einem Fehlerereignis genau sagen, welche Arbeitsschritte und Eingaben zu der Fehlerauslösung geführt haben, welche Programme er über das Internet installiert hat usw.? Anwender sind fehlerfrei, sie machen alles richtig, nur das Programm ist schlecht. Sind wir doch einmal ehrlich zu uns selbst: Gibt es ein Softwarehaus, das von sich selbst behaupten kann, unter der Last des Termindrucks nicht schon mindestens einmal ein Programm ausgeliefert zu haben, das eine unzureichende Testphase durchlaufen hat?

Es gibt aber auch eine Fehlergattung, die nicht das unplanmäßige Beenden des Programms nach sich zieht, sondern nur falsche Ergebnisse liefert: die logischen Fehler. Dies ist insbesondere deshalb sehr unangenehm, weil solche Fehler oftmals sehr spät erkannt werden und weitreichende Konsequenzen haben können. Denken Sie einmal daran, welche Auswirkungen es haben könnte, wenn ein Finanz- und Buchhaltungsprogramm (FIBU) einen falschen Verkaufspreis ermitteln würde. Es kommt zu keinem offensichtlichen Laufzeitfehler, der anzeigt, dass etwas unkorrekt abläuft. Solche Fehler können unter Umständen sogar die Existenz eines gesamten Unternehmens gefährden. Um dieses Dilemma zu vermeiden, muss die Software ausgiebig getestet werden, wobei der Debugger der Entwicklungsumgebung wesentliche Unterstützung bietet. Wir werden uns dem Thema des Debuggens in Abschnitt 9.2 zuwenden.

In diesem Abschnitt wollen wir uns mit der Fehlergattung auseinander setzen, die zur Auslösung einer Ausnahme zur Laufzeit führt und die verschiedensten Ursachen haben kann:

gp  Anwender geben unzulässige Werte ein.
gp  Es wird versucht, eine nicht vorhandene Datei zu öffnen.
gp  Es wird versucht, eine Nulldivision durchzuführen.
gp  Beim Zugriff auf eine Objektmethode ist der Bezeichner der Objektvariablen noch nicht initialisiert.
gp  Eine Netzwerkverbindung ist instabil.
gp  ...

Die Liste ist schier endlos lang sein. Aber allen Fehlern ist eins gemein: Sie führen zum Absturz des Programms, wenn der auftretende Fehler nicht behandelt wird.


Galileo Computing

9.1.1 Laufzeitfehler erkennen  downtop

Das folgende Codefragment demonstriert einen typischen Laufzeitfehler und die daraus resultierenden Konsequenzen. Die Aufgabe, die das Programm ausführen soll, ist dabei recht simpel: Es soll eine bestimmte Textdatei öffnen und deren Inhalt an der Konsole ausgeben.


Imports System.IO
Module Module1
Sub Main()
Dim myFile As StreamReader = New StreamReader("C:\Text.txt")
Console.WriteLine(myFile.ReadToEnd())
Console.ReadLine()
myFile.Close()
End Sub
End Module

Die Klassenbibliothek des .NET Frameworks bietet zum Öffnen einer Datei die Klasse StreamReader im Namespace System.IO an. Einer der Konstruktoren dieser Klasse erwartet den vollständigen Pfad zu der zu öffnenden Datei:


Public Sub New(path As String)

Aus dem Datenstrom können mit Read einzelne Zeichen gelesen werden und mit ReadLine eine komplette Zeile. ReadToEnd liest den ganzen Datenstrom vom ersten bis zum letzten Zeichen. Im Beispiel wird die letztgenannte Methode benutzt und die Rückgabe aus dem Datenstrom als Argument der WriteLine-Methode der Console übergeben.


Hinweis

In Kapitel 12 erhalten Sie weitere Informationen zu den Dateioperationen.


Solange die angegebene Datei existiert, wird die Anwendung fehlerfrei ausgeführt. Übergeben Sie dem Konstruktor der Klasse StreamReader allerdings eine Zeichenfolge auf eine nicht vorhandene Datei, wird die Laufzeit der Anwendung mit der unten abgebildeten Fehlermeldung unterbrochen und das Programm danach beendet.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 9.1     Fehlermeldung einer nicht behandelten Ausnahme

Fehler dieser Art, die auch als Exceptions bezeichnet werden, müssen schon während der Programmierung erkannt und behandelt werden. Die Fehlerbehandlung hat die Zielsetzung, dem Anwender beispielsweise durch eine Eingabekorrektur die Fortsetzung des Programms zu ermöglichen oder – schlimmstenfalls – zumindest alle notwendigen Daten zu sichern, bevor das Programm beendet wird.

Es war in der Vergangenheit gängige Praxis, über den Rückgabewert einer Prozedur den Benutzer vom Erfolg oder Misserfolg einer Operation in Kenntnis zu setzen. Viele Funktionen des Win32-API (API = Application Programming Interface, Funktionssammlung der betriebssystemnahen Funktionen) arbeiten nach diesem Prinzip. Schematisch sieht die Codestruktur einer Prozedur, die diesen Richtlinien genügt, folgendermaßen aus:


Public Function OpenFile(ByVal strFile As String) As Boolean
' Anweisungen, die zu einem Fehler führen können
If(kein Fehler aufgetreten) Then
' Anweisungen
´ Return True
Else
' Anweisungen
Return False
End If
End Function

Der Aufrufer der fiktiven Methode OpenFile kann den Rückgabewert der Funktion nutzen, um den weiteren Programmablauf an die Situation anzupassen:


Public Sub Caller()
' Anweisungen
If OpenFile Then
' Dateiinhalt anzeigen
Else
' OpenFile mit anderem Argument aufrufen
End If
End Sub

Dieser Technik haftet allerdings ein gravierender Nachteil an: Der Rückgabewert einer Funktion muss nicht zwangsläufig entgegengenommen werden, denn ein Aufruf der Methode OpenFile mit


OpenFile("C:\MichGibtEsNicht.txt")

ist ebenfalls syntaktisch korrekt. Damit verschwindet der alarmierende Rückgabewert in den Tiefen des Speichers, was im Fehlerfall natürlich zum Absturz der laufenden Anwendung führen kann.

Eine zweite Schwäche dieser Technik ist der Informationsgehalt des Rückgabewertes. Es ist nicht bestimmbar, welche Umstände zum Fehler geführt haben. Existiert die Datei überhaupt? Wenn ja, war es eine zusammengebrochene Netzwerkverbindung, die zu der Fehlermeldung geführt hat, oder ist die Datei möglicherweise bereits geöffnet? Die Alternative, anstelle eines booleschen Werts mehrere verschiedene Rückgabewerte als Identifizierer eines bestimmten Fehlercodes vorzuschreiben, ist keine gute, weil sie zu wenig verallgemeinernd und portierbar ist. Darüber hinaus ist eine solche Lösung mit den Konzepten der objektorientierten Programmierung nur schwer vereinbar.

Visual Basic stellt eine bessere Programmiertechnik bereit, mit der die angesprochenen Probleme der Vergangenheit angehören: Ein auftretender Laufzeitfehler – auch als Ausnahme oder Exception bezeichnet – erzeugt ein Fehlerobjekt, das die Fehlerinformationen kapselt. Dieses Objekt sucht nach einer Behandlungsroutine (Exceptionhandler), die sich des Fehlerobjekts annimmt und die durch das Fehlerobjekt beschriebene Ausnahme den Anforderungen der aktuellen Anwendung entsprechend behandelt.


Galileo Computing

9.1.2 Die Behandlung von Exceptions  downtop

Ganz im Sinne der Objektorientierung werden Ausnahmen als Objekte angesehen. Das Grundprinzip lässt sich wie folgt beschreiben:

1. Es tritt ein Laufzeitfehler auf, der eine Exception auslöst. Eine Exception kann auch unter vom Entwickler festgelegten Umständen ausgelöst werden und muss nicht zwangsläufig systemgebunden sein.
       
2. Die Ausnahme wird entweder vom fehlerverursachenden Programmteil direkt behandelt oder weitergeleitet. Der Empfänger einer weitergeleiteten Ausnahme steht seinerseits in der Pflicht: Entweder er behandelt die Ausnahme, oder er delegiert sie ebenfalls weiter.
       

Das Programm wird unplanmäßig beendet, wenn eine Exception von keiner der aufgerufenen Methoden behandelt wird.


Galileo Computing

9.1.3 Die »Try ... Catch«-Anweisung  downtop

Sehen wir uns nun zunächst die Syntax der einfachsten Ausnahmebehandlung an:


Try
' Anweisungen
Catch ex As AusnahmetypException
' Anweisungen
End Try
' Anweisungen
End Sub

Der Try-Block zwischen dem einleitenden Try und der Catch-Anweisung beinhaltet die Anweisung(en), die potenziell eine Ausnahme verursachen können. Tritt kein Laufzeitfehler auf, werden alle Anweisungen im Try-Block ausgeführt, danach setzt das Programm hinter End Try seine Arbeit fort. Verursacht eine der Anweisungen innerhalb des Try-Blocks jedoch einen Fehler, werden alle folgenden Anweisungen innerhalb dieses Blocks ignoriert, und der Programmablauf führt den Code hinter der Catch-Anweisung aus. Hier könnten beispielsweise Benutzereingaben gesichert oder Netzwerkverbindungen getrennt werden. Nach der Abarbeitung des Catch-Blocks wird das Programm mit der Anweisung fortgesetzt, die dem End Try folgt.

Kann die Laufzeitumgebung keine Übereinstimmung zwischen dem Typ der ausgelösten Exception und dem angegebenen Typ im Parameter des Catch-Statements feststellen, gilt die Ausnahme als nicht behandelt – mit der Konsequenz des unkontrollierten Beendens des Programms.

Wir wollen diese Ausführungen nun mit einem praktischen Beispiel testen. Dazu greifen wir wieder auf das Beispiel am Anfang dieses Kapitels zurück, in dem eine Datei geöffnet und an der Konsole ausgegeben werden soll.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 9\TryCatchDemo_1
' ----------------------------------------------------------
Imports System.IO
Module Module1
Sub Main()
Dim str As String
Dim dataStream As StreamReader
Console.Write("Welche .txt-Datei soll ")
Console.WriteLine("geöffnet werden?")
str = Console.ReadLine()
' Fehlerbehandlung einleiten
Try
' die folgende Anweisung kann zu einer Exception füh-ren
dataStream = New StreamReader(str)
Console.WriteLine("--- Dateianfang ---")
Console.WriteLine(dataStream.ReadToEnd())
Console.WriteLine("--- Dateiende -----")
dataStream.Close()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert
' eine fehlerspezifische Meldung ausgeben
Console.WriteLine("Datei nicht gefunden.")
End Try
Console.WriteLine("Nach der Exception-Behandlung")
Console.ReadLine()
End Sub
End Module

Starten Sie das Programm, und geben Sie nach der Aufforderung einen gültigen Zugriffspfad auf eine Datei an, z.  B.:


C:\MeineProgramme\Lebenslauf.txt

Die Datei wird geöffnet und ihr Inhalt an der Konsole angezeigt. Das Programm wird bis zum Catch-Statement ausgeführt und verzweigt danach zu der Anweisung, die dem End Try folgt, was durch eine Konsolenausgabe bestätigt wird. Das ist der Normalfall – oder ist vielleicht eher die Angabe einer nicht existierenden Datei als normal anzusehen? Wie dem auch sei, unser kleines Programm ist in der Lage, auch damit umzugehen, wie Sie noch sehen werden.

Die Anweisung, die eine Ausnahme im obigen Beispiel auslösen könnte, ist der Aufruf des Konstruktors der Klasse StreamReader, dem eine Pfadangabe als Argument übergeben wird:


dataStream = New StreamReader(str)

Entscheidend ist, dass eine Anweisung, die einen Laufzeitfehler verursachen könnte, innerhalb des Try-Blocks codiert ist, damit sie der Überwachung der Fehlerbehandlungsroutine untersteht.

Das Beispiel ist noch sehr naiv ausgelegt, denn der Versuch zum Öffnen einer Datei kann auch aus anderen Gründen scheitern und zu einer Ausnahme führen – beispielsweise weil eine Netzwerkverbindung unterbrochen oder die Datei bereits geöffnet ist. Jede Fehlerursache wird durch ein speziell »geschultes« Ausnahmeobjekt beschrieben. Der Beispielcode oben ist aber noch so entwickelt, dass er nur auf einen ganz bestimmten Fehler reagieren kann, nämlich auf den, der durch die Pfadangabe zu einer nicht existierenden Datei ausgelöst wird. Versuchen Sie beispielsweise, auf eine Datei in einem nicht vorhandenen Verzeichnis zuzugreifen, wird die Laufzeit des Programms weiterhin außerplanmäßig beendet, weil dieser Ausnahmetyp nicht durch die von uns angeführte FileNotFoundException beschrieben wird.

Bei einer Ausnahme verzweigt der Programmablauf in die Catch-Anweisung und vergleicht den Typ der ausgelösten Exception mit dem Parametertyp der Catch-Anweisung. Stimmen beide überein, werden die Anweisungen des Catch-Blocks, hier bis End Try ausgeführt. In unserem Beispiel wird eine Ausnahme aufgrund einer falschen Dateiangabe behandelt, beschrieben durch ein Objekt vom Typ FileNotFoundException. Dessen Eigenschaften können Sie abfragen, beispielsweise um sich eine allgemein gehaltene Fehlerbeschreibung ausgeben zu lassen:


Console.WriteLine(e.Message)

Nach der vollständigen Ausführung des Catch-Blocks wird das Programm ordnungsgemäß mit den sich daran anschließenden Anweisungen fortgesetzt. Damit haben wir unser Ziel erreicht: Obwohl ein Laufzeitfehler aufgetreten ist, kontrollieren wir weiterhin das Laufzeitverhalten. Gleichzeitig arbeiten wir nach objektorientierten Prinzipien.


Galileo Computing

9.1.4 Behandlung mehrerer Exceptions  downtop

Vielleicht haben Sie sich vorhin die Frage gestellt, wo die Definition der Klasse FileNotFound-Exception zu finden ist und woher die Kenntnis stammt, dass eine solche Ausnahme beim Aufruf des Konstruktors der Klasse StreamReader ausgelöst werden kann?

Die Antwort ist trivial: Die Angaben sind in der Dokumentation zur .NET-Klassenbibliothek zu finden. Ein Blick in die Dokumentation des in unserem Beispiel eingesetzten Stream- Reader-Konstruktors verrät, dass neben der abgefangenen Ausnahme noch einige weitere möglich sind, unter anderem:

gp  ArgumentException
gp  ArgumentNullException
gp  DirectoryNotFoundException
gp  IOException

Die Ausnahme ArgumentException wird ausgelöst, wenn der Anwender an der Konsole nach der Aufforderung zur Eingabe des Pfades keine Angabe macht und das Programm sofort fortsetzt. Da dieser Ausnahmetyp von der Fehlerbehandlung unseres Beispiels bisher nicht abgefangen wird, liegt hier eine weitere Gefahrenquelle vor.

Eine ähnliche Ausnahme, ArgumentNullException, würde beim Konstruktoraufruf eine andere Codierung voraussetzen, nämlich einen uninitialisierten String. Dieser Fehler kann in unserem Beispiel nicht auftreten.

Haben Sie schon versucht, einen Ordnernamen einzugeben, der sich nicht im aktuellen oder angegebenen Laufwerk befindet? Es kommt zu einer Ausnahme vom Typ DirectoryNotFoundException, die von uns ebenfalls berücksichtigt werden muss. Der letzten in der Dokumentation aufgeführten Ausnahme IOException kommt eine besondere Bedeutung zu, der wir uns noch später widmen werden.

Wenn eine Datei geöffnet wird, können also mehrere grundsätzlich unterschiedliche Ausnahmen auftreten, die alle behandelt werden müssen. Um auf verschiedene Ausnahmen spezifisch reagieren zu können, geben wir in der Fehlerbehandlungsroutine mehrere Catch-Anweisungsblöcke an, von denen jeder einen bestimmten Ausnahmetyp behandelt.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 9\TryCatchDemo_2
' ----------------------------------------------------------
Imports System.IO
Module Module1
Sub Main()
Dim str As String
Dim dataStream As StreamReader
Console.Write("Welche .txt-Datei soll ")
Console.WriteLine("geöffnet werden?")
str = Console.ReadLine()
' Fehlerbehandlung einleiten
Try
' die folgende Anweisung kann zu einer Exception führen
dataStream = New StreamReader(str)
Console.WriteLine("--- Dateianfang ---")
Console.WriteLine(dataStream.ReadToEnd())
Console.WriteLine("--- Dateiende -----")
dataStream.Close()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert,
' eine fehlerspezifische Meldung ausgeben
Console.WriteLine("Datei nicht gefunden.")
Catch e As ArgumentException
' falls der Anwender einen Leerstring übergibt
Console.WriteLine("Sie müssen eine Datei angeben.")
Catch e As DirectoryNotFoundException
' falls das Verzeichnis nicht existiert
Console.WriteLine("Der Ordner existiert nicht.")
End Try
Console.WriteLine("Nach der Exception-Behandlung")
Console.ReadLine()
End Sub
End Module

Jeder Catch-Zweig fängt einen bestimmten Fehler ab. Wird eine Ausnahme ausgelöst, werden die Catch-Zweige zur Laufzeit so lange der Reihe nach angesteuert, bis der Typ gefunden wird, der die ausgelöste Ausnahme beschreibt. Im Beispiel oben wird also zuerst geprüft, ob der Exception eine nicht existierende Datei zugrunde liegt (FileNotFoundException). Hat der Fehler eine andere Ursache, wird geprüft, ob der Anwender dem Konstruktor einen Leer- string übergeben hat (ArgumentException). War das auch nicht der Fall, wird zuletzt der aufgetretene Fehler mit DirectoryNotFoundException verglichen.

Grundsätzlich wird nur ein Catch-Anweisungsblock ausgeführt, nämlich der, der den Fehler behandeln kann. Beachten Sie, dass die Reihenfolge der Catch-Zweige nicht beliebig sein darf. Wir werden auf diese Thematik in Abschnitt 9.1.8 eingehen.


Galileo Computing

9.1.5 Die »Finally«-Anweisung  downtop

Nehmen wir an, eine Methode hätte eine Datenbankverbindung aufgebaut oder eine Datei geöffnet, die vor dem Beenden der Methode wieder freigegeben werden muss. Tritt nach dem Öffnen der Ressource ein Laufzeitfehler auf, muss sichergestellt werden, dass diese Ressource in jedem Fall ordentlich geschlossen wird.

Die strukturierte Fehlerbehandlung bietet dazu optional noch eine weitere, bislang noch nicht erwähnte Klausel an, in der solche Aufräumarbeiten erledigt werden können: die Finally-Klausel, die unmittelbar dem letzten Catch-Block folgt, falls sie angegeben wird.


Try
' Anweisungen
Catch e As Exception1
' Anweisungen
Catch e As Exception2
' Anweisungen
Finally
' Anweisungen
End Try
' Anweisungen

Fehlerbehandlungsroutinen, die eine Finally-Klausel enthalten, führen deren Anweisungsblock unabhängig davon aus, ob eine Ausnahme ausgelöst worden ist oder nicht. Es gibt nur eine einzige Randbedingung: Das Programm muss zumindest in den Try-Anweisungsblock eintreten.

Fassen wir an dieser Stelle alle Begleitumstände zusammen, die zur Ausführung der Anweisungen im Finally-Block führen.

gp  Es wird keine Ausnahme ausgelöst. Der Try-Block wird komplett abgearbeitet, danach verzweigt das Programm zur Finally-Klausel und wird anschließend mit der Anweisung fortgesetzt, die dem Finally-Anweisungsblock folgt.
gp  Der Code löst eine Exception aus, die mit einer Catch-Klausel behandelt wird. Von der fehlerauslösenden Codezeile im Try-Block aus sucht die Laufzeit nach der passenden Catch-Klausel, führt diese aus und verzweigt zur Finally-Klausel. Anschließend wird die Anweisung ausgeführt, die dem Finally-Anweisungsblock folgt.

Welchem Zweck dient der »Finally«-Anweisungsblock?

Der Try-Anweisungsblock enthält die Anweisungen, die potenziell zu einer Exception führen können. Kommt es zu einer Ausnahme, wird sie in einem passenden Catch-Block behandelt. Anschließend wird zuerst der Code im Finally-Block ausgeführt, danach auch noch alle Anweisungen, die dem Finally-Block folgen. Anscheinend ist es völlig bedeutungslos, ob die den Catch-Blöcken folgenden Anweisungen im Finally-Block implementiert werden oder nicht: Es ist im ersten Moment kein Unterschied im Laufzeitverhalten festzustellen.

Dennoch gibt es einen. Nehmen wir an, dass Sie nach der Behandlung der Ausnahme im Catch-Block die Methode verlassen wollen, weil die Anweisungen, die sich den Catch-Blöcken anschließen, nicht ausgeführt werden sollen. Sie werden dann im Catch-Anweisungsblock mit Return die Methode verlassen, z.  B.:


...
Catch e As IrgendeineException e
' Anweisungen
Return
Finally
' Anweisungen

In diesem Fall ist Return aber nicht von der durchschlagenden Konsequenz, wie wir dieses Statement bisher kennen gelernt haben. Die Methode wird nämlich nicht sofort verlassen, sondern es wird zunächst nach dem Finally-Anweisungsblock gesucht. Ist er vorhanden, wird er garantiert ausgeführt. Es kommt aber nicht mehr zu der Ausführung der Anweisungen, die dem Finally-Block möglicherweise noch folgen.


Galileo Computing

9.1.6 Das Weiterleiten von Ausnahmen  downtop

Eine Ausnahme muss in jedem Fall behandelt werden, um das laufende Programm vor dem Absturz zu bewahren. In den vorhergehenden Beispielen haben Sie gesehen, wie Laufzeitfehler mit Try und Catch behandelt werden, damit das Programm ordentlich fortgesetzt werden kann. Kennzeichnend war bisher, dass wir eine auftretende Ausnahme in der Methode behandelten, in der sie auftrat. Es stellt sich nun die Frage, wie mit einer Exception umgegangen wird, wenn eine Methode eine zweite aufruft und es in der aufgerufenen Methode zu einem Fehler kommt.

Um in dieser Ausgangssituation auf die Ausnahme zu reagieren, bieten sich zwei Alternativen an:

1. Die Ausnahme wird in der aufgerufenen Methode mit Try ... Catch behandelt.
       
2. Die Ausnahme wird an die aufrufende Methode weitergeleitet.
       

Wie eine Ausnahme in der auslösenden Methode behandelt wird, haben Sie bereits anhand unseres Beispiels gesehen: Die fehleranfällige Anweisung steht innerhalb des Try-Anweisungsblocks, ein eventuell ausgelöster Fehler wird mit Catch behandelt.

Sie müssen aber nicht unbedingt eine Ausnahme in der Methode behandeln, in der sie ausgelöst worden ist, d.  h., man verzichtet auf Try ... Catch. Nehmen wir an, die Methode MyProcA ruft die Methode MyProcB auf, in der es zu einer Ausnahme kommt, die dort nicht behandelt wird. In diesem Fall wird die Ausnahme an den Aufrufer, hier also die Methode MyProcA, weitergereicht, die dann in der Verantwortung steht, sich um die Exception zu kümmern. Die aufrufende Komponente kann dann ganz spezifisch und individuell auf den Fehler reagieren. Wir wollen uns diese Programmiertechnik nun ansehen.


' -----------------------------------------------------
' Beispiel: ...\Kapitel 9\TryCatchDemo_3
' -----------------------------------------------------
Imports System.IO
Module Module1
Sub Main()
Dim str As String
Console.Write("Welche .txt-Datei soll ")
Console.WriteLine("geöffnet werden?")
str = Console.ReadLine()
Dim obj As FileData = New FileData(str)
Try
obj.GetData()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert
Console.WriteLine("Die Datei existiert nicht.")
Catch e As ArgumentException
' falls der Anwender einen Leerstring übergibt
Console.WriteLine("Leerstring übergeben.")
Catch e As DirectoryNotFoundException
' falls das Verzeichnis nicht existiert
Console.WriteLine("Verzeichnis existiert nicht.")
End Try
Console.ReadLine()
End Sub
End Module
Class FileData
Private file As String
Private dataStream As StreamReader
Public Sub New(ByVal str As String)
file = str
End Sub
Public Sub GetData()
dataStream = New StreamReader(file)
Console.WriteLine("--- Dateianfang ---")
Console.WriteLine(dataStream.ReadToEnd())
Console.WriteLine("--- Dateiende -----")
dataStream.Close()
End Sub
End Class

Die Fähigkeit, eine Datei zu öffnen und zu lesen, wird nun an die Methode GetData der Klasse FileData delegiert. FileData stellt einen Konstruktor bereit, der als Argument den Pfad zu der zu öffnenden Datei entgegennimmt. Die Anweisung, die zu einer Exception führen kann, ist im Code der Methode GetData zu finden – genauer gesagt, handelt es sich hierbei um den StreamReader-Konstruktoraufruf. Wie bereits in den beiden vorhergehenden Beispielen könnte der Anwender eine Datei angeben, die im angegebenen Pfad nicht existiert, er könnte den Verzeichnisnamen falsch schreiben usw.

Trotz dieser Kenntnis lehnt die Methode GetData jede Verantwortung beim Auftreten eines Fehlers ab, weil innerhalb der Methode keine Ausnahmebehandlung implementiert ist.

Wird aus einer Methode heraus eine zweite aufgerufen und tritt in der aufgerufenen ein Laufzeitfehler auf, sucht die Laufzeitumgebung zunächst in der fehlerauslösenden Methode nach einer Ausnahmebehandlung. Ist hier keine implementiert, wird die Ausnahme dem Aufrufer übergeben. Ist dieser intelligent und nimmt sich des ausgelösten Fehlers an, ist den Anforderungen Genüge getan, und die Anwendung wird klaglos weiterlaufen. Anders sieht es allerdings aus, wenn die aufrufende Methode die Ausnahme nicht behandelt: Die Anwendung stürzt unweigerlich ab.

Dieses Schema, nach einer Fehlerbehandlungsroutine zu suchen, setzt sich fort, falls im Aufrufstack mehrere Methoden aufgelistet sind, die sich in einer Aufrufverkettung befinden. Ruft beispielsweise die Methode MethodA die Methode MethodB auf und MethodB wiederum MethodC, befinden sich MethodA, MethodB und MethodC im Aufrufstack. Die Abarbeitung erfolgt in umgekehrter Reihenfolge: Zuerst wird MethodC vom Stack genommen und ausgeführt, danach MethodB und zum Schluss MethodA. Eine Ausnahme, die in MethodC auftritt und dort nicht behandelt wird, wird an MethodB weitergeleitet. Enthält auch MethodB keine passende Fehlerbehandlung, steht MethodA in der Pflicht.

Da wir in unserem Beispiel natürlich genau wissen, welche Ausnahmen in der Methode GetData auftreten können, kann die Behandlung komplett in Main erfolgen. Sie muss sogar hier erfolgen, denn wenn eine Ausnahme nicht spätestens hier abgefangen wird, ist das Ende der laufenden Anwendung nicht nur eine theoretische Vision.

Die ausgelöste Ausnahme direkt an den Aufrufer weiterleiten

Im Beispiel TryCatchDemo_3 wird die Behandlung des Fehlers dem Aufrufer überlassen: Die aufgerufene Methode überträgt die Verantwortung der Behandlung ihrem Aufrufer, der nach eigenem Ermessen auf die Situation reagieren kann.

Man kann den Fehler bereits in der fehlerbehafteten Methode, also »vor Ort«, ganz allgemein behandeln. Das entspricht der Situation, die anfangs beschrieben wurde. Unter Umständen soll aber die aufrufende Methode zusätzlich in die Lage versetzt werden, ganz individuell und nach eigenem Ermessen zu reagieren. Sehen wir uns dies wieder an einem Beispiel an.


' -----------------------------------------------------
' Beispiel: ...\Kapitel 9\TryCatchDemo_4
' -----------------------------------------------------
Imports System.IO
Module Module1
Sub Main()
Dim str As String
Console.Write("Welche .txt-Datei soll ")
Console.WriteLine("geöffnet werden?")
str = Console.ReadLine()
Dim obj As FileData = New FileData(str)
Try
obj.GetData()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert
Console.WriteLine("Die Datei existiert nicht.")
Catch e As ArgumentException
' falls der Anwender einen Leerstring übergibt
Console.WriteLine("Leerstring übergeben.")
Catch e As DirectoryNotFoundException
' falls das Verzeichnis nicht existiert
Console.WriteLine("Verzeichnis existiert nicht.")
End Try
Console.ReadLine()
End Sub
End Module
Class FileData
Private file As String
Private dataStream As StreamReader
' ----- Konstruktor -----
Public Sub New(ByVal str As String)
file = str
End Sub
' ----- Instanzmethode -----
Public Sub GetData()
Try
dataStream = New StreamReader(file)
Console.WriteLine("--- Dateianfang ---")
Console.Write(dataStream.ReadToEnd())
Console.WriteLine("--- Dateiende ---")
dataStream.Close()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert
Throw e
Catch e As ArgumentException
' falls der Anwender einen Leerstring übergibt
Throw e
Catch e As DirectoryNotFoundException
' falls das Verzeichnis nicht existiert
Throw e
End Try
End Sub
End Class

Tritt ein Laufzeitfehler auf, wird in der GetData-Methode ein entsprechendes Exception-Objekt erzeugt, das im Catch-Block aufgefangen und an den Aufrufer weitergeleitet wird. Diese Weiterleitung erfolgt mit dem Schlüsselwort Throw unter Übergabe der Referenz des ausgelösten Exception-Objekts:


Throw e

Obwohl in den Catch-Blöcken unseres Beispiels nur eine Anweisung enthalten ist, können hier durchaus auch weitere Operationen erfolgen.

Der Aufrufer empfängt die Referenz auf die Ausnahme und muss nun seinerseits in einer eigenen Behandlungsroutine darauf reagieren. Die Reaktion darf dabei auch minimal sein, nämlich einzig und allein durch die Angabe einer passenden Catch-Klausel, deren Anweisungsblock leer bleibt:


Catch e As FileNotFoundException

Werfen wir noch einmal einen Blick in die .NET-Dokumentation des von uns benutzten Konstruktors der Klasse StreamReader. Im Quellcode dieses Konstruktors sieht es ähnlich aus wie in unserer Methode: Er reicht die Ausnahmen an den Aufrufer mit einem Throw-Konstrukt weiter. Wir nehmen die Ausnahmen in der GetData-Methode in Empfang, behandeln sie allerdings nicht lokal, sondern reichen sie weiter.

Gezwungen werden wir nicht, alle Exceptions des StreamReader-Konstruktors zu behandeln. Können wir garantieren, dass eine bestimmte Ausnahme nicht auftreten kann, dürfen wir ruhigen Gewissens auf die Fehlerbehandlung verzichten. Genau das haben wir auch mit der Ausnahme ArgumentNullException getan.

Eine spezifische Ausnahme auslösen und weiterleiten

Die folgende Variante, eine ausgelöste Exception an den Aufrufer weiterzuleiten, ähnelt sehr stark der im letzten Abschnitt. Zusätzlich werden aber nun spezifische Zustandsinformationen an den Benutzer zurückgegeben.

Dazu wird die ausgelöste Exception regelrecht in eine andere verpackt. Die in der Catch-Klausel erzeugte Ausnahme ist eine äußere, die im zweiten Argument übergebene Referenz ist die innere Exception. Der Aufrufer erhält damit eine direkte Referenz auf die ausgelöste Ausnahme sowie eine spezifische Fehlerbeschreibung.


' ---------------------------------------------------------
' Beispiel: ...\Kapitel 9\TryCatchDemo_5
' ---------------------------------------------------------
Imports System.IO
Module Module1
Sub Main()
...
Try
obj.GetData()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert
Console.WriteLine(e.Message + "- Die Datei existiert nicht.")
Catch e As ArgumentException
' falls der Anwender einen Leerstring übergibt
Console.WriteLine(e.Message + "- Leerstring übergeben.")
Catch e As DirectoryNotFoundException
' falls das Verzeichnis nicht existiert
Console.WriteLine(e.Message + "- Verzeichnis existiert nicht.")
End Try
Console.ReadLine()
End Sub
End Module
Class FileData
Private file As String
Private dataStream As StreamReader
' ----- Konstruktor -----
Public Sub New(ByVal str As String)
file = str
End Sub
' ----- Instanzmethode -----
Public Sub GetData()
Try
dataStream = New StreamReader(file)
Console.WriteLine("--- Dateianfang ---")
Console.Write(dataStream.ReadToEnd())
Console.WriteLine("--- Dateiende ---")
dataStream.Close()
Catch e As FileNotFoundException
' falls die angegebene Datei nicht existiert
Throw (New FileNotFoundException("In GetData()", e))
Catch e As ArgumentException
' falls der Anwender einen Leerstring übergibt
Throw (New ArgumentException("In GetData()", e))
Catch e As DirectoryNotFoundException
' falls das Verzeichnis nicht existiert
Throw (New DirectoryNotFoundException("In GetData()", e))
End Try
End Sub
End Class

Um die Referenz der Ausnahme an den Aufrufer weiterzuleiten, wird wiederum das Throw-Statement benutzt.

Das Throw-Statement löst eine Ausnahme erneut aus, entweder durch Weitergabe der Referenz eines konkreten Exception-Objekts oder durch das Erzeugen eines neuen Exception-Objekts mit dem New-Operator:


Throw Referenz der ausgelösten Exception
Throw(New Exceptiontyp())

Als Argument wird im Beispiel TryCatchDemo_5 eine neue Instanz derselben Ausnahme erzeugt. Das funktioniert deshalb, weil jede Exception auf einer Klassendefinition basiert, die über öffentliche Konstruktoren verfügt. Wenn Sie den parametrisierten Konstruktor aufrufen, der sowohl eine Zeichenfolge als auch die ursprüngliche Exception selbst entgegennimmt, also:


Public Sub New(String, Exception)

können Sie die weiterzureichende Exception um eine spezifische Mitteilung ergänzen, indem Sie den überladenen Konstruktor aufrufen:


Throw(New Exceptiontyp(String, e) ...

Damit steht dem Aufrufer ein Maximum an Informationen zur Verfügung, um in angemessener Weise auf die Ausnahme reagieren zu können, die in einer aufgerufenen Methode ausgelöst wird. Er muss nur noch die zusätzlich übermittelten Informationen auswerten. Dazu genügt der Abruf der Eigenschaft Message des über den Parameter e referenzierten Exception-Objekts.


Galileo Computing

9.1.7 Die Hierarchie der Exceptions  downtop

.NET unterstützt die Anwendungsentwickler durch eine Vielzahl vordefinierter Ausnahmeklassen. Ganz im Sinn eines guten objektorientierten Ansatzes sind alle in einer Klassenhierarchie geordnet. Hinter dieser Strukturierung steckt die Absicht, ausgehend von einer Basis-ausnahme eine immer weiter gehende Verzweigung zu ermöglichen, was letztendlich zu immer spezialisierteren Ausnahmen führt.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 9.2     Auszug aus der Exception-Hierarchie

An der Spitze der Hierarchie befindet sich die Klasse Exception, von der alle anderen Ausnahmeklassen abgeleitet sind. In der zweiten Ebene werden die abgeleiteten Ausnahmen kategorisiert, um dem gesamten Schema eine geordnete Struktur zu verleihen.

Die Ausnahme ArgumentException ihrerseits ist Basisklasse weiterer, spezialisierterer Ausnahmen. Fast alle von SystemException abgeleiteten Klassen sind Mitglieder des Namespace System und beschreiben systembedingte Ausnahmen.

Ausnahmen, die mit der Ein- und Ausgabe im Zusammenhang stehen, werden von der allgemeinen Klasse IOException im Namespace System.IO beschrieben. Zu dieser Kategorie zählen auch die Ihnen aus dem Beispiel bekannten Ausnahmen FileNotFoundException und DirectoryNotFoundException.

Eine weitere wichtige Exception ist ApplicationException, von der keine weiteren Subklassen abgeleitet sind. Beabsichtigen Sie, eine benutzerdefinierte Ausnahmen zu codieren, sollte diese Klasse abgeleitet werden.


Galileo Computing

9.1.8 Die Reihenfolge der »Catch«-Klauseln  downtop

Die Klasse Exception bildet die Wurzel der gesamten Ausnahmehierarchie, die Klassen IOException, ArgumentException und ApplicationException sind davon abgeleitete Klassen und stellen somit spezialisiertere Formen ihrer Basisklasse dar.

Ausgehend von IOException sind die beiden Ausnahmen FileNotFoundException und DirectoryNotFoundException weitere Spezialisierungen, wobei beide auf dieselbe direkte Basisklasse zurückzuführen sind. Dies ist eine ganz wesentliche Erkenntnis, die für uns bei der Entwicklung einer Strategie zum Behandeln von Ausnahmen von entscheidender Bedeutung ist, denn es gilt die folgende Regel:


Die einzelnen Catch-Klauseln werden in der Reihenfolge ihres Auftretens abgearbeitet. Sobald die in der Catch-Klausel angegebene Ausnahme zuweisungskompatibel zum aufgetretenen Fehler ist, wird der Anweisungsblock in der entsprechenden Klausel durchlaufen.


Diesen Sachverhalt wollen wir uns an einem Beispiel verdeutlichen:


Try
' Anweisungen
Catch e As FileNotFoundException
' Anweisungen
Catch e As DirectoryNotFoundException
' Anweisungen
Catch e As ArgumentNullException
' Anweisungen
Catch e As ArgumentException
' Anweisungen
Catch e As Exception
' Anweisungen

Nehmen wir an, es wird eine Ausnahme des Typs ArgumentNullException ausgelöst. Das Programm durchläuft nun der Reihe nach alle Catch-Klauseln: Zuerst wird geprüft, ob die Ausnahme vom Typ FileNotFoundException ist, danach erfolgt eine Überprüfung auf DirectoryNotFoundException. Beide beschreiben nicht den Typ unserer fiktiv angenommenen Ausnahme, da diese nicht aus den beiden vorgenannten Klassen abgeleitet ist. Wäre das der Fall, würden die Gesetze der Objektorientierung greifen, die besagen, dass das Objekt einer Subklasse gleichzeitig auch ein Objekt seiner Basisklasse ist. Erst in der dritten Catch-Klausel wird die Laufzeit fündig, und die Anweisungen innerhalb dieses Blocks werden abgearbeitet.

Nun stellen wir uns vor, es sei ArgumentOutOfRangeException ausgelöst worden. Die ersten drei Catch-Klauseln werden abgewiesen, da sie wiederum nicht dem Typ der ausgelösten Ausnahme entsprechen. In der vierten wird die Laufzeit fündig, da die Klasse ArgumentException die Basisklasse der tatsächlich ausgelösten Ausnahme ist (vergleichen Sie bitte mit der Abbildung 9.2). Jetzt kommen die Regeln der impliziten Konvertierung zum Tragen, die besagen, dass das Objekt einer Subklasse gleichzeitig auch ein Objekt seiner Basisklasse ist. Es werden also die Anweisungen im Anweisungsblock der vierten Catch-Klausel zum Behandeln der Ausnahme ausgeführt.

Kommt es zu einem Laufzeitfehler, der weder als IOException noch als ArgumentException beschrieben werden kann, wird die Ausnahme in jedem Fall durch die letzte Catch-Klausel aufgefangen, weil grundsätzlich jede Ausnahme aus der Klasse Exception abgeleitet wird.

Ausgehend von der ersten Catch-Klausel werden die angegebenen Ausnahmen immer weiter verallgemeinert. Trifft das Programm auf die erste passende, die in der Klassenhierarchie zumindest Basisklasse der ausgelösten ist, wird die Ausnahme behandelt.

Theoretisch wäre es natürlich möglich und erscheint im ersten Moment auch verlockend, mit nur einer einzigen Catch-Klausel unter Angabe einer allgemeinen Exception jeden denkbaren Fehler zu behandeln. Einerseits wäre damit in jedem Fall die Laufzeit der Anwendung gerettet, andererseits kann damit eine dem tatsächlichen Fehler angemessene Behandlung natürlich nicht sichergestellt werden. Sowohl die Anwendung als auch der Anwender würden darüber im Unklaren gelassen, welche Operation zu einem Abbruch geführt hat.

Viele Umstände können zum Auslösen einer Exception führen. Besonders kompliziert wird es, wenn damit gerechnet werden muss, dass trotz ausgiebiger Testläufe nicht alle möglichen Ausnahmesituationen erfasst werden konnten. Es ist dann sinnvoll, in der letzten Catch-Klausel alle nicht berücksichtigten Fälle mit einer allgemeinen Exception aufzufangen. Obwohl der Fehler damit keine angemessene Behandlung erfährt, besteht zumindest die Möglichkeit, ihn zu protokollieren, möglicherweise die Daten zu speichern und das Programm eventuell ordentlich zu beenden.


Galileo Computing

9.1.9 Die Basisklasse »Exception«  downtop

Die Basisklasse aller Ausnahmen bildet die Klasse Exception, die im Namespace System zu finden ist. Grundsätzlich sind alle anderen Ausnahmen von dieser Klasse abgeleitet. Es lohnt sich daher, einen Blick auf die Eigenschaften dieser Klasse zu werfen.


Tabelle 9.1     Die Eigenschaften der Klasse »Exception«

Eigenschaft Beschreibung
HelpLink Verweist auf eine Hilfedatei, die diese Ausnahme beschreibt.
InnerException Liefert die Referenz auf die tatsächliche Ausnahme. Diese Information dient dazu, auf geeignetere Weise auf die Ausnahme zu reagieren.
Message Gibt einen String mit der Beschreibung des aktuellen Fehlers zurück.
Source Liefert einen String zurück, der die Anwendung oder das Objekt beschreibt, die bzw. das den Fehler ausgelöst hat.
StackTrace Liefert einen String zurück mit der aktuellen Aufrufreihenfolge aller Methoden.
TargetSite Liefert die Methode zurück, in der die Ausnahme ausgelöst worden ist.


Galileo Computing

9.1.10 Benutzerdefinierte Exceptions  toptop

Die .NET-Klassenbibliothek stellt sehr viele Ausnahmeklassen zur Verfügung. Diese decken wohl die meisten Ausnahmen ab, die in Abhängigkeit vom Status der laufenden Anwendung eintreten können. Jede dieser Klassen ist von der gemeinsamen Basisklasse Exception abgeleitet, sei es direkt oder indirekt über eine verzweigte Vererbungshierarchie.

In manchen Situationen ist das Angebot allerdings nicht ausreichend, weil das Bedürfnis besteht, unter ganz bestimmten Umständen eine Ausnahme auszulösen, die nicht in der Klassenbibliothek vordefiniert ist. Dabei würde es sich um eine anwendungsspezifische Ausnahme handeln. In diesem Fall ist es kein Problem, nach Belieben eigene Ausnahmetypen zu konstruieren, die nur unter bestimmten Bedingungen ausgelöst werden. Die Architekten der Klassenbibliothek haben an diese Möglichkeit gedacht und stellen eine Klasse bereit, die als Basisklasse aller benutzerdefinierten Ausnahmen dient: ApplicationException. Wollen Sie eine eigene Ausnahme entwickeln, müssen Sie eine Klasse definieren, die von Application-Exception abgeleitet ist.

Es steht Ihnen natürlich völlig frei, daraus selbst eine individuell verzweigte Vererbungskette zu entwickeln. Konventionsgemäß sollte jede Klasse einen beschreibenden Bezeichner haben, dem am Ende das Suffix Exception angehängt wird.

Wir wollen nun eine benutzerdefinierte Ausnahme an einem Beispiel entwickeln. Dazu benutzen wir noch einmal unser Beispiel der Circle-Klasse aus den Kapiteln 4 bis 6 und schauen uns hier stellvertretend die Eigenschaft Radius an:


Public Property Radius() As Double
Get
Return dblRadius
End Get
Set(ByVal Value As Double)
If Value >= 0 Then
dblRadius = Value
Else
Me.OnMeasureError(Me, Value)
End If
End Set
End Property

Wie Sie sich sicherlich noch erinnern, hatten wir festgelegt, dass die Übergabe an die Eigenschaft Radius größer oder gleich 0 sein muss. Diese Bedingung wurde – wie Sie oben sehen können – durch eine If-Anweisung geprüft. Liegt ein ungültiger Zuweisungsversuch vor, wird ein Ereignis ausgelöst.

Dieser Lösungsansatz ist gut, aber da wir nun gelernt haben, mit Ausnahmen umzugehen, bietet sich eine Alternative an. Wir können bei einer unzulässigen Zuweisung auch eine Ausnahme auslösen, anstatt den Anwender über ein Ereignis zu informieren. Dazu wollen wir eine eigene Exception bereitstellen, die wir als RadiusException bezeichnen wollen.


' Benutzerdefinierte Exception
Public Class RadiusException
Inherits ApplicationException
Public Sub New()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub
Public Sub New(ByVal message As String, ByVal inner As Exception)
MyBase.New(message, inner)
End Sub
End Class

In der Klasse sind drei Konstruktoren mit unterschiedlichen Parameterlisten definiert. In der Basisklasse ApplicationException ist der Konstruktor mit derselben Parameterliste überladen. Wir leiten die Parameter deshalb jeweils mit MyBase an den passenden Konstruktor der Basisklasse weiter.

Die Klasse ApplicationException ist ein direkter Nachfahre der Klasse Exception und erbt somit auch deren Mitglieder. Schauen Sie in die Tabelle 9.1, werden Sie feststellen, dass die oberste Basisklasse die schreibgeschützte Eigenschaft Message veröffentlicht, die eine allgemein gehaltene Beschreibung der ausgelösten Ausnahme enthält. Abgesehen vom parameterlosen Konstruktor, dem eine Aufgabe zukommt, wenn aus unserer benutzerdefinierten Klasse eine weitere abgeleitet wird, nehmen die beiden anderen eine fehlerbeschreibende Zeichenfolge im Parameter message entgegen.

Arbeiten wir mit einer Verkettung von Ausnahmen, kann eine Referenz auf die tatsächliche Ausnahme dem zweiten Parameter inner des letztgenannten Konstruktors übergeben werden. Dieser Konstruktor wird von der Throw-Anweisung benutzt, wenn wir mit


Throw(New RadiusException("Unzulässiger Radius", e))

eine neue Exception auswerfen und dabei eine passende individuelle Meldung sowie die Referenz auf die ursprüngliche Ausnahme an den Aufrufer übergeben.

Tatsächlich haben wir nun schon eine vollwertige, anwendungsspezifische Ausnahmeklasse entwickelt, die wir in der Klasse Circle nutzen können:


Public Property Radius() As Double
Get
Return dblRadius
End Get
Set(ByVal value As Double)
If (value >= 0) Then
dblRadius = value
Else
Throw (New RadiusException("Unzulässiger Radius"))
End If
End Set
End Property

Beachten Sie, dass das Throw-Statement diesmal nicht innerhalb eines Catch-Blocks eingesetzt wird. Es wird keine Ausnahme weitergeleitet, an dieser Stelle wird eine Ausnahme vom Typ RadiusException mit einer passenden Nachricht ausgelöst.

Wenden wir uns nun dem Benutzer der Klasse Circle zu. Dies wird vermutlich ein Entwickler sein, der die Klassenimplementierung nicht kennt und auf eine ordentliche Dokumentation zu Ihrer Klasse angewiesen ist. Er befindet sich also in derselben Position wie Sie, wenn Sie eine Klasse des .NET Frameworks in Ihrer Anwendung nutzen möchten: Auch Sie werden erwarten, dass die operativen Fähigkeiten der Klasse nicht nur allgemein beschrieben sind, sondern auch Details enthalten, die für Ihre Entwicklungsarbeit unverzichtbar sind. Dazu gehört auch die Angabe, unter welchen Umständen bestimmte Ausnahmen ausgelöst werden.

Das folgende Beispiel zeigt, wie der Code hinsichtlich der Übergabe des Radius in einer Clientanwendung aussehen könnte:


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 9\UserDefinedException
' ----------------------------------------------------------
Module Module1
Sub Main()
Dim obj As Circle = New Circle()
Console.Write("Der Radius beträgt: ")
Do
Loop While (Not SetRadius(obj))
Console.WriteLine("Kreisradius = {0}", obj.Radius)
Console.ReadLine()
End Sub
Public Function SetRadius(ByVal obj As Circle) As Boolean
Try
obj.Radius = Console.ReadLine()
Return True
Catch e As RadiusException
Console.WriteLine(e.Message)
Console.Write("Erneute Radiuseingabe: ")
Return False
Catch e As Exception
Console.WriteLine("Unbekannte Ausnahme!")
Console.WriteLine(e.Message)
Return False
End Try
End Function
End Module

Es ist eine Methode SetRadius implementiert, um den Clientcode zu modularisieren. SetRadius liefert einen booleschen Wert zurück, der in der aufrufenden Main-Prozedur von der Do-Schleife dazu benutzt wird, bei einer unzulässigen Wertzuweisung noch einmal dieselbe Methode aufzurufen.

Aus der Kenntnis der Dokumentation heraus wird die klassenspezifische Ausnahme RadiusException in SetRadius behandelt. Eine gute Idee ist es, zusätzlich noch eine weitere, allgemeine Ausnahme für alle anderen, nicht berücksichtigten Ausnahmen im letzten Catch-Block vorzusehen. Wenn Sie dies als unnötig erachten sollten, dann starten Sie die Anwendung, nachdem Sie die Behandlung der allgemeinen Ausnahme Exception auskommentiert haben, und geben Sie anstelle einer Zahl ein Zeichen des Alphabets ein – es kommt zu einem Laufzeitfehler. Vielleicht wird Sie das überzeugen, denn die Fehleingabe löst nun nicht mehr den Fehler RadiusException aus, sondern eine andere, nicht explizit angegebene Ausnahme. Zur Information lassen wir uns die Beschreibung dieser Ausnahme mit


Console.WriteLine(e.Message)

an der Konsole anzeigen. Sie lautet:


Die Eingabezeichenfolge hat das falsche Format.

und ist auf die systembedingte Ausnahme FormatException zurückzuführen. Betrachtet man es sehr eng, könnte man sagen, dass der Entwickler der Klasse Circle hier eine Nachlässigkeit begangen hat. Eigentlich sollte davon ausgegangen werden, dass die Implementierung und die Dokumentation der Eigenschaft Radius auch diesen Fall berücksichtigen.

 <<   zurück
  
  Zum Katalog
Zum Katalog: Visual Basic 2005
Visual Basic 2005
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Visual C# 2005






 Visual C# 2005


Zum Katalog: Fortgeschrittene Programmierung mit Visual C# 2005






 Fortgeschrittene
 Programmierung
 mit Visual C# 2005


Zum Katalog: Das Programmierhandbuch SQL Server 2005






 Das Programmier-
 handbuch
 SQL Server 2005


Zum Katalog: Einstieg in Visual Basic 2005






 Einstieg in
 Visual Basic 2005


Zum Katalog: Einstieg in Visual C# 2005






 Einstieg in
 Visual C# 2005


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo








Copyright © Galileo Press 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de