Das Testen eines Programms ist unumgänglich. Dieses Kapitel stellt Protokollierung und Debugging vor. Am Ende werden noch Klassen grafisch visualisiert, ohne den Kontakt zum Code zu verlieren.
9 Code erstellen und debuggen
9.1 Ausnahmen 

Bereits in Abschnitt 2.8, »Fehlerbehandlung«, habe ich Ihnen die Wirkungsweise der strukturierten Fehlerbehandlung mit Try/Catch/Finally gezeigt. In diesem Abschnitt beschränke ich mich auf einige fehlende Aspekte.
Hinweis |
Leider kann im Programmcode weder getestet werden, ob eine Methode eine Ausnahme auslösen könnte, noch muss deklariert werden, ob bzw. wie mit einer möglichen Ausnahme umzugehen ist. Der Compiler hilft Ihnen in diesem Punkt nicht weiter. |
9.1.1 Methodenaufrufe 

In »älteren« Programmiersprachen gibt es keine gesonderte Fehlerbehandlung. Meist wird ein Fehler durch einen speziellen Rückgabewert einer Funktion signalisiert. Das hat den enormen Nachteil, dass der Aufrufer nicht gezwungen ist, sich mit dem Fehler auseinanderzusetzen – er darf ihn ignorieren. In .NET dagegen werden Fehler durch das Auslösen einer Ausnahme signalisiert, die schlicht zum Abbruch eines Programms führt, wenn sie nicht behandelt wird. Damit dieser Zwang durchgesetzt werden kann, sind Ausnahmen unabhängig von dem, was Sie programmieren. Insbesondere funktionieren sie auch bei beliebig tief verschachtelten Methodenaufrufen. Nicht Sie, sondern die Laufzeitumgebung kümmert sich um den geordneten Ausstieg, bis eine Stelle erreicht ist, an der ein Fehler in einem Catch-Zweig behandelt wird. Damit geht einher, dass Sie Fehler an einer beliebigen, geeigneten Stelle in der Aufrufhierarchie der Methoden behandeln können: direkt beim Aufruf, in derselben Methode, in der übergeordneten Methode oder bei einem Aufrufer der Methode.
Bezüglich dieser Transparenz gegenüber dem Aufrufstack gibt es eine Besonderheit zu beachten. In Abschnitt 2.8.4, »Try/Catch/Finally«, haben wir uns mit dem Finally-Zweig beschäftigt. Er wird sowohl bei Fehlerfreiheit als auch im Fehlerfall ausgeführt. Wie weit das geht, zeigt das folgende Beispiel. Der erste Aufruf von Kehrwert läuft fehlerfrei, im zweiten wird im Catch-Zweig ein Return ausgeführt.
'...\Lauf\Ausnahmen\ReturnFinally.vb |
Option Strict On
Namespace Lauf
Module ReturnFinally
Function Kehrwert(ByVal nenner As Integer) As Double
Try
Return 1 / nenner
Catch ex As Exception
Return Double.NaN
Finally
Console.WriteLine("Arbeit beendet.")
End Try
End Function
Sub Test()
Console.WriteLine("1/5={0}", Kehrwert(5))
Console.WriteLine("1/0={0}", Kehrwert(0))
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt, dass mit und ohne Return der Finally-Zweig immer ausgeführt wird.
Arbeit beendet.
1/5=0,2
Arbeit beendet.
1/0=+unendlich
Damit ist der Finally-Zweig der richtige Ort für Anweisungen, die immer – unabhängig vom Erfolg – ausgeführt werden müssen.
9.1.2 Hierarchie der Ausnahmen 

Bei der großen Anzahl an Ausnahmeklassen im .NET Framework – ich habe 636 gezählt – ist eine Organisation unumgänglich. Daher sind auch diese Klassen in einer Vererbungshierarchie organisiert, von der ich hier exemplarisch einen Ausschnitt mit den wichtigsten aus dem Namensraum System und allen aus dem Namensraum System.IO zeige.
Object
+Exception
+ApplicationException
+SystemException
+AccessViolationException
+ArgumentException
| +ArgumentNullException
| +ArgumentOutOfRangeException
| +DuplicateWaitObjectException
+ArithmeticException
| +DivideByZeroException
| +NotFiniteNumberException
| +OverflowException
+FormatException
| +UriFormatException
| +IO.FileFormatException
+IndexOutOfRangeException
+InvalidCastException
+InvalidOperationException
| +ObjectDisposedException
+NotImplementedException
+NotSupportedException
| +PlatformNotSupportedException
+NullReferenceException
+OutOfMemoryException
| +InsufficientMemoryException
+RankException
+StackOverflowException
+TypeInitializationException
+IO.InternalBufferOverflowException
+IO.InvalidDataException
+IO.IOException
+IO.DirectoryNotFoundException
+IO.DriveNotFoundException
+IO.EndOfStreamException
+IO.FileLoadException
+IO.FileNotFoundException
+IO.PathTooLongException
+IO.PipeException
Die Hierarchie wirkt sich an zwei Stellen besonders aus. Zum Ersten wird eine Referenz einer Ausnahme auf eine ihrer Basisklassen als allgemeiner betrachtet als die Ausnahme selbst. Da die Catch-Zweige immer in der Reihenfolge von speziell zu allgemein sortiert werden müssen, tauchen solche Referenzen weiter unten auf als die Ausnahme selbst. Außerdem kann durch die Ist-eine-Beziehung das Auffangen einer Basisklassen-Ausnahme alle Kindklassen-Ausnahmen mit erfassen, sodass keine vergessen werden kann. Im folgenden Beispiel wird die spezielle Ausnahme DivideByZeroException vor der allgemeineren ArithmeticException behandelt. In der Methode Test werden Werte übergeben, die jede der drei Ausnahmen in den Catch-Zweigen auslösen.
'...\Lauf\Ausnahmen\Hierarchie.vb |
Option Strict On
Namespace Lauf
Module Hierarchie
Class Zahl : Public Wert As Integer : End Class
Function Rechnung(ByVal z As Zahl) As Integer
Try
Return z.Wert * z.Wert \ z.Wert
Catch ex As DivideByZeroException
Console.Write(ex.Message & ": ") : Return Integer.MaxValue
Catch ex As NullReferenceException
Console.Write(ex.Message & ": ") : Return Integer.MinValue
Catch ex As ArithmeticException
Console.Write(ex.Message & ": ") : Return 0
End Try
End Function
Sub Test()
Dim z As New Zahl()
z.Wert = 0 : Console.WriteLine(Rechnung(Nothing))
z.Wert = 0 : Console.WriteLine(Rechnung(z))
z.Wert = Integer.MinValue : Console.WriteLine(Rechnung(z))
Console.ReadLine()
End Sub
End Module
End Namespace
Die dritte Zeile zeigt, wie die »vergessene« Ausnahme OverflowException von dem Catch-Zweig mit deren Basisklasse ArithmeticException erfasst wurde.
Object reference not set to an instance of an object.: –2147483648
Attempted to divide by zero.: 2147483647
Arithmetic operation resulted in an overflow.: 0
Der zweite Effekt der Vererbungshierarchie der Ausnahmen ist, dass ausnahmslos alle Ausnahmen von der gemeinsamen Basisklasse Exception abgeleitet sind und deren Funktionalität nutzen können. Tabelle 9.1 zeigt deren Eigenschaften.
Eigenschaft | Beschreibung | |
Data |
Zusätzliche benutzerdefinierte Informationen (IDictionary) |
R |
HelpLink |
Verweist auf eine Hilfedatei, die diese Ausnahme beschreibt |
|
HResult |
Fehlercode zur Interoperabilität mit COM-Klassen (Protected) |
|
InnerException |
Referenz auf die tatsächliche Ausnahme. Diese Information dient dazu, auf geeignetere Weise auf die Ausnahme zu reagieren. |
R |
Message |
Gibt einen String mit der Beschreibung des aktuellen Fehlers zurück. |
R |
Source |
Beschreibung der fehlerauslösenden Anwendung oder des Objekts |
|
StackTrace |
String mit der aktuellen Aufrufreihenfolge aller Methoden |
R |
TargetSite |
Methode, in der die Ausnahme ausgelöst worden ist |
R |
Besonders hinweisen möchte ich auf InnerException, die oft benutzt wird, wenn eine Ausnahme aufgefangen wird und eine andere auslöst. Wenn vor dem Auslösen die innere Ausnahme gesetzt wird, kann der Aufrufer den »wahren« Grund der Ausnahme ermitteln.
9.1.3 Eigene Ausnahmen 

Sie sollten Fehler, die spezifisch für Ihre Anwendung sind, durch eigene Ausnahmen kennzeichnen. Diese müssen sich direkt oder indirekt von Exception ableiten. Damit Anwender nicht denken, dass eine Ihrer Ausnahmen Teil von .NET ist, empfiehlt sich die Ableitung von ApplicationException. Beide genannten Ausnahmeklassen haben die gleichen Konstruktoren, von denen Sie einen – implizit oder explizit – in dem Konstruktor Ihrer Ausnahmeklasse aufrufen müssen.
Public Sub New() |
Von den 636 Ausnahmen, die ich in .NET gezählt habe, enden nur 30 nicht auf Exception. Sie sollten sich bei eigenen Ausnahmeklassen auch an diese Konvention halten, sie macht Ihren Code für andere leichter lesbar. Das folgende Beispiel definiert eine kleine Ausnahmeklassenhierarchie: ApplicationException->FinanzamtException->BuchungsException. In der Me-thode Summe wird ein Fehler mit einer BuchungsException quittiert. Der Aufrufer von Summe, die Methode Abschreibung, verpackt den Fehler in die allgemeinere Ausnahme FinanzamtException. In der Methode Test wird Abschreibung mit Werten aufgerufen, die einen Fehler auslösen. Der Catch-Zweig ist spezifisch für diese Anwendung und behandelt nur hier definierte Ausnahmen. Im Falle eines Fehlers wird nicht nur die Fehlermeldung ausgegeben, sondern auch der dem Fehler zugrunde liegende Fehler.
'...\Lauf\Ausnahmen\Eigene.vb |
Option Strict On
Namespace Lauf
Module Eigene
Class FinanzamtException : Inherits ApplicationException
Sub New(nachricht As String, grund As Exception)
MyBase.New(nachricht, grund)
End Sub
Sub New(nachricht As String)
MyBase.New(nachricht)
End Sub
End Class
Class BuchungsException : Inherits FinanzamtException
Sub New(nachricht As String)
MyBase.New(nachricht)
End Sub
End Class
Function Summe(wert As Short, jahre As Short) As Short
Try
Return wert \ jahre
Catch ex As Exception
Throw New BuchungsException("Null Jahre.")
End Try
End Function
Function Abschreibung(wert As Short, jahre As Short) As Short
Try
Return Summe(wert, jahre)
Catch ex As BuchungsException
Throw New FinanzamtException("Buchungsfehler.", ex)
End Try
End Function
Sub Test()
Try
Abschreibung(1000, 0)
Catch ex As FinanzamtException
Console.WriteLine("Fehler beim Finanzamt: {0}", ex.Message)
Console.WriteLine("Wahrer Grund: {0}", ex.InnerException)
End Try
Console.ReadLine()
End Sub
End Module
End Namespace
Die Formatierung der inneren Ausnahme als String beinhaltet unter anderem den Inhalt der Eigenschaft StackTrace und gibt so weitere Hinweise zur Fehlerbeseitigung.
Fehler beim Finanzamt: Buchungsfehler.
Wahrer Grund: Ausnahmen.Lauf.Eigene+BuchungsException: Null Jahre.
at Ausnahmen.Lauf.Eigene.Summe(Int16 wert, Int16 jahre)
in M:\VisualStudioWS\Lauf\Ausnahmen\Eigene.vb:line 21
at Ausnahmen.Lauf.Eigene.Abschreibung(Int16 wert, Int16 jahre)
in M:\VisualStudioWS\Lauf\Ausnahmen\Eigene.vb:line 26
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.