Lies in den Artikel rein und unten bekommst Du ein unschlagbares Angebot!
Nur wenige Access-Programmierer statten ihre Anwendungen mit einer ordentlichen Fehlerbehandlung aus. Für viele ist dies ein leidiges Thema. Die Fehlerbehandlung sorgt im Optimalfall sowohl unter VBA als auch unter C# dafür, dass eine Anwendung stabiler wird und nach dem Auftreten von Laufzeitfehlern nicht unerwartet reagiert oder sogar abstürzt. Während es unter VBA nur wenige Konstrukte gibt, um eine Fehlerbehandlung zu implementieren, bietet C# schon eine Menge mehr. Dieser Artikel liefert eine Einführung und zeigt, wie Sie die von VBA gewohnten Techniken unter C# einsetzen.
Die minimale Fehlerbehandlung unter VBA sieht so aus, dass Sie vor fehlerträchtigen Anweisungen die eingebaute Fehlerbehandlung mit der folgenden Programmzeile deaktivieren:
On Error Resume Next
Alle folgenden Fehler in der aktuellen Routine und in solchen, die von dieser Routine aufgerufen werden, werden nicht behandelt. Dadurch gibt es zwar immerhin keine Fehlermeldung und es werden keine Variablen geleert, was beim Auftreten unbehandelter Fehler auftreten kann. Allerdings erledigen fehlerhafte Zeilen ihre Aufgabe nicht, was zu Folgefehlern (auch logischen Fehlern) in den folgenden Anweisungen führen kann.
Spätestens mit dem Ende der Routine endet die Deaktivierung der eingebauten Fehlerbehandlung. Vorher erhalten Sie dies mit der folgenden Anweisung:
On Error Goto 0
Dadurch reagiert VBA wieder wie gewohnt mit entsprechenden Fehlermeldungen auf Fehler.
Dazwischen haben Sie Gelegenheit, benutzerdefiniert auf die auftretenden Fehler zu reagieren – beispielsweise, indem Sie den Wert der Eigenschaft Number des Err-Objekts auslesen und für die interessanten Werte entsprechende Fehlerbehandlungen hinzufügen.
Dies kann beispielsweise wie folgt aussehen:
On Error Resume Next Debug.Print 1/0 Select Case Err.Number Case 11 MsgBox "Fehler: Teilen durch 0." End Select On Error Goto 0
Unter C# gibt es zur Fehlerbehandlung andere Möglichkeiten, die Sie in den folgenden Abschnitten kennen lernen.
Warum überhaupt eine Fehlerbehandlung?
Um auf Nummer sicher zu gehen, wollen wir zuvor noch einmal kurz auf die Gründe für die Programmierung einer benutzerdefinierten Fehlerbehandlung eingehen. Wenn Sie Code programmieren, der fehlerhafte Eingaben zulässt und keine entsprechende benutzerdefinierte Fehlerbehandlung aufweist, erhalten Sie beispielsweise bei einer Konsolenanwendung eine Meldung plus Fehlerbehandlungsfenster wie in Bild 1.
Bild 1: Unbehandelte Ausnahme bei einer Konsolenanwendung
Wenn Sie einen solchen Fehler in einer WPF-Anwendung auslösen, erhalten Sie noch nicht einmal einen kleinen Hinweis auf den Fehler im Code, der zu dieser Ausnahme führte (siehe Bild 2).
Bild 2: Unbehandelte Ausnahme bei einer Windows-Anwendung
Noch übler wird es, wenn nicht Sie selbst die Software bedienen, sondern ein Kunde: Erstens macht dies nie einen guten Eindruck, zweitens kann der Kunde auf Basis der hier gelieferten Fehlerinformationen kaum helfen, den Fehler zu finden.
Also sollten Sie dafür sorgen, dass der Kunde einen aussagekräftigen Text als Fehlermeldung erhält, der ihn beispielsweise bei einem Eingabefehler auf die Ursache des Fehlers hinweist oder aber ihm die Informationen liefert, die er zur Behebung des Problems an den Entwickler der Software weitergeben kann.
Der wichtigste Punkt beim Implementieren einer benutzerdefinierten Fehlerbehandlung etwa unter VBA ist jedoch die stabile Fortsetzung der Anwendung: Wenn ein Fehler aufgetreten ist, beispielsweise durch eine Fehleingabe oder einen fehlerhaften Zugriff auf ein Element, dann sollte die Anwendung nach der Ausgabe der Fehlermeldung fortgesetzt werden können und nicht einfach abbrechen. Dabei ist außerdem sicherzustellen, dass die Funktion und der stabile Zustand der Anwendung durch den Fehler nicht beeinträchtigt werden. Bei VBA war das mitunter ganz einfach deshalb nicht der Fall, weil durch unbehandelte Fehler Variableninhalte gelöscht oder Objekte zerstört wurden. Wurden die Zeilen, die den Fehler auslösten, hingegen per On Error Resume Next einer benutzerdefinierten Fehlerbehandlung zugeführt, statt einfach unbehandelte Fehler auszulösen, war zumindest schon einmal der Inhalt und der Zustand der Variablen sichergestellt.
Fehlerhafter Code
Schauen wir uns das Beispiel an, das zum Auslösen der Ausnahme der Konsolenanwendung führte. Den Code finden Sie in Listing 1. Die Zeile, die das Ergebnis der Division der Werte der Variablen Dividend und Divisor ermitteln soll, löst die Ausnahme aus, wenn der Divisor zuvor den Wert 0 zugewiesen bekommen hat – der Fehler tritt also durch eine Division durch 0 auf.
static void Main(string[] args) { Console.WriteLine("Geben Sie den Dividend und den Divisor als ganze Zahlen ein."); Console.WriteLine("Dividend:"); decimal Dividend = Convert.ToDecimal(Console.ReadLine()); Console.WriteLine("Divisor:"); decimal Divisor = Convert.ToDecimal (Console.ReadLine()); decimal Quotient = Dividend / Divisor; Console.WriteLine("Der Quotient lautet: {0}", Quotient); Console.ReadLine(); }
Listing 1: Methode, die einen Fehler auslöst, wenn als Divisor der Wert 0 angegeben wird
Wenn Sie die Anwendung debuggen, also diese von Visual Studio aus etwa mit der Taste F5 starten, erhalten Sie einige Fehlerinformationen mehr. Außerdem markiert Visual Studio gleich die fehlerhafte Zeile (siehe Bild 3). Es gibt sogar noch Tipps zur Problembehandlung.
Bild 3: Auslösen einer Ausnahme unter C#
Wenn Sie möchten, erhalten Sie auch noch weitere Details, und zwar durch einen Mausklick auf den Link Details anzeigen …, der den Dialog aus Bild 4 öffnet.
Bild 4: Details zu einer Ausnahme beim Debuggen in Visual Studio
Für Fehler wie diese gibt es einige Beispiele – die Eingabe unzulässiger Werte wie in diesem Beispiel, Zugriff auf nicht vorhandene Dateien et cetera.
try…catch statt On Error Resume Next
Unter C# können Sie Laufzeitfehler in sogenannten try…catch-Blöcken behandeln. Das sieht dann so aus, dass Sie den Code, der einen Fehler auslösen könnte, in den try-Block packen und den Code, der ausgelöst werden soll, wenn innerhalb des try-Blocks ein Fehler auftritt, in den catch-Block.
In unserem Fall wollen wir die Zeile, welche die Division durchführt, in den try-Block überführen. Gleichzeitig fügen wir dort auch die Zeile ein, welche das Ergebnis aus der Variablen Quotient in der Konsole ausgibt – sonst ergibt dies einen Syntaxfehler, weil Quotient nicht in jedem Falle deklariert und initialisiert wird.
In den catch-Block schreiben wir eine Anweisung, welche einen Hinweis auf das Auftreten eines Fehlers in der Konsole ausgibt (siehe Listing 2). Wie Sie hier erkennen, wird die folgende Anweisung, die ja das Ergebnis der Berechnung ausgeben sollte, ignoriert – dies gilt grundsätzlich für alle Anweisungen, die der fehlerhaften Anweisung folgen.
try { decimal Quotient = Dividend / Divisor; Console.WriteLine("Der Quotient lautet: {0}", Quotient); } catch { Console.WriteLine("Ups! Es ist ein Fehler beim Rechnen aufgetreten!"); }
Listing 2: Abfangen eines Fehlers per try…catch-Block
Nun erhält der Benutzer zwar auch keine wesentlich aussagekräftigere Meldung, aber dafür wird das Programm auch nicht einfach abgebrochen. Außerdem haben wir ja auch noch gar nicht geprüft, um was für einen Fehler es sich handelt – dies prüfen wir in der folgenden Version.
Die Anwendung wird dann mit den Anweisungen fortgesetzt, die nach dem Ende des catch-Blocks folgen. Dies erkennen Sie in diesem Beispiel daran, dass das Betätigen der Eingabetaste das Programm beendet, weil die letzte Console.ReadLine()-Methode ausgeführt wird (siehe Bild 5).
Bild 5: Benutzerdefinierte Fehlermeldung beim Auftreten der Ausnahme
Exception statt Err
Unter C# heißt das Objekt, das die per Code auswertbaren Fehlerinformationen enthält, Exception. Es entspricht etwa dem Objekt Err unter VBA, das ja mit seinen Eigenschaften Description oder Number oft hilfreiche Informationen liefert.
Damit Sie dieses Objekt nutzen können, müssen Sie es für den catch-Zweig als Parameter hinzufügen, also wie in Listing 3 mit catch (Exception e).
catch (Exception e) { Console.WriteLine("Meldung des Exception-Objekts:\n{0}\n", e.Message); Console.WriteLine("Umfangreiche Informationen:\n{0}\n", e.GetBaseException().ToString()); Console.WriteLine("Typ der Ausnahme:\n{0}", e.GetType().ToString()); }
Listing 3: Ausstatten des catch-Block mit einigen weiteren Informationen
Die folgenden drei Zeilen des Beispiels geben verschiedene Informationen auf die Konsole aus, die Sie auch in Bild 6 sehen. Die erste ist der Inhalt der Eigenschaft Message. Die Eigenschaft GetBaseException liefert den Text, den auch ein unbehandelter Fehler auf die Konsole zaubert. Schließlich gibt uns die Eigenschaft GetType hilfreiche Informationen, wenn es darum geht, den Typ der Ausnahme zu ermitteln. In diesem Fall lautet der Typ DivideByZeroException. Damit können wir später gezielt auf bestimmte Fehler reagieren.
Bild 6: Erweiterte Fehlermeldung des Exception-Objekts
Allgemeine Exception
Was wir im vorherigen Beispiel getan haben, war die Behandlung einer allgemeinen Exception. Die Klasse Exception ist quasi die Mutter aller Fehlerklassen. Es gibt eine ganze Reihe von Fehlerklassen, die von dieser Klasse abgeleitet sind.
Wenn Sie im catch-Zweig einer Ausnahmebehandlung in Klammern ein Objekt des Typs Exception übergeben, fangen Sie damit alle möglichen Ausnahmen ab. Dies entspricht etwa dem Case Else-Zweig in einer VBA-Fehlerbehandlung wie der folgenden:
Select Case Err.Number Case 11 ''Geteilt durch Null ''Fehler behandeln Case 0 ''kein Fehler, nichts ist zu tun Case Else ''Alle übrigen Fehlernummern behandeln End Select
Nun wollen wir noch wissen, wie wir einen speziellen Fehler abfangen, in diesem Fall die DivideByZeroException. Wenn Sie nur diesen einen Fehler abfangen möchten, reicht die folgende Variante aus:
try { decimal Quotient = Dividend / Divisor; Console.WriteLine("Der Quotient lautet: {0}", Quotient); } catch (DivideByZeroException e) { Console.WriteLine("Der Dividend darf nicht 0 sein."); }
Der catch-Zweig wird in diesem Fall nur angesteuert, wenn eine DivideByZero-Ausnahme ausgelöst wird. Im Falle eines jeden anderen Fehlers tritt eine unbehandelte Ausnahme auf.
Um einen weiteren, andersartigen Fehler auszulösen und behandeln zu können, ziehen wir die übrigen Anweisungen der Methode wie in Listing 4 ebenfalls in den try-Block hinein. Außerdem fügen wir neben dem catch-Block, der sich um den Fehler kümmert, der beim Teilen durch 0 auftritt, einen weiteren catch-Block hinzu. Dieser soll wieder alle übrigen Fehler abfangen.
public static void Andere_Exception() { try { Console.WriteLine("Geben Sie den Dividend und den Divisor als ganze Zahlen ein."); Console.WriteLine("Dividend:"); decimal Dividend = Convert.ToDecimal(Console.ReadLine()); Console.WriteLine("Divisor:"); decimal Divisor = Convert.ToDecimal(Console.ReadLine()); decimal Quotient = Dividend / Divisor; Console.WriteLine("Der Quotient lautet: {0}", Quotient); } catch (DivideByZeroException e) { Console.WriteLine("Der Dividend darf nicht 0 sein."); } catch (Exception e) { Console.WriteLine("Es ist ein Fehler aufgetreten: {0}", e.GetType().ToString()); } Console.ReadLine(); }
Listing 4: Abfangen eines speziellen und eines allgemeinen Fehlers per try…catch-Block
Wenn wir nun beispielsweise für die erste Console.ReadLine-Anweisung keine Zahl, sondern eine Zeichenkette eingeben, erhalten wir einen weiteren Fehler. Dieser wird ausgelöst, da wir das Ergebnis der Eingabe direkt in einen Wert des Datentyps decimal umwandeln wollen. Das gelingt mit einer Zeichenkette natürlich nicht, also liefert dies eine weitere Ausnahme – wie Bild 7 zeigt.
Bild 7: Fehler durch die Eingabe von Daten im falschen Format
Nachdem wir wissen, dass dies die Ausnahme FormatException auslöst, fügen wir auch diese als weiteren catch-Block zur Fehlerbehandlung hinzu:
try { ... } catch (DivideByZeroException e) { Console.WriteLine("Der Dividend darf nicht 0 sein."); } catch (FormatException e) { Console.WriteLine("Bitte geben Sie eine ganze Zahl ein."); } catch (Exception e) { Console.WriteLine("Es ist ein Fehler aufgetreten: {0}", e.GetType().ToString()); }
Auf ähnliche Weise legen wir für alle Fehler, die auftreten können, eine Ausnahmebehandlung an (zumindest für die, die wir bereits erkennen können – der Benutzer wird sicher noch weitere Schwachstellen aufdecken …).
Die Reihenfolge der catch-Blöcke spielt natürlich eine übergeordnete Rolle, gerade wenn Sie eine allgemeine Ausnahmebehandlung (mit catch (Exception e)) und speziellere Ausnahmebehandlungen (wie mit catch (DivideByZeroException)) implementieren. Die catch-Blöcke werden nämlich immer in der angegebenen Reihenfolge abgearbeitet. Wenn Sie also direkt in der ersten catch-Anweisung auf die allgemeine Ausnahme Exception prüfen, wird diese bei jeder denkbaren Ausnahme angesteuert, da ja alle anderen Ausnahmen von Exception erben. Aber Visual Studio beugt dem direkt vor, denn untergeordnete Ausnahmen müssen immer vor übergeordneten Ausnahmen abgearbeitet werden. Wenn wir versuchen, die allgemeine Exception-Ausnahme vor der DivideByZeroException-Ausnahme zu behandeln, erhalten wir einen Kompilierfehler (siehe Bild 8).
Bild 8: Exceptions müssen in der Hierarchie von unten nach oben abgearbeitet werden.
Hierarchie der Exceptions
Damit kommen wir zurück zur Mutter aller Fehlerklassen und den untergeordneten Elementen. Woher erfahren wir, in welcher Hierarchie-Ebene sich eine …Exception-Klasse befindet und welche die übergeordneten Klassen sind?
Dazu klicken Sie etwa im Code-Fenster mit der rechten Maustaste auf den Text DivideByZeroException und wählen dort den Eintrag Definition einsehen aus. Dies öffnet einen gelb hinterlegten Bereich, der die öffentliche Klasse beschreibt, die von der Klasse ArithmeticException abgeleitet ist (siehe Bild 9). Diese Abhängigkeit erkennen Sie an dem Doppelpunkt zwischen den beiden Klassennamen (DivideByZeroException : ArithmeticException).
Bild 9: Die Definition einer Klasse liefert Hinweise auf die übergeordnete Klasse
Die Klasse ArithmeticException wird also vermutlich sämtliche Ausnahmen behandeln, die durch die Verwendung von Rechenoperatoren unter C# ausgelöst werden, wobei DivideByZeroException nur ein Spezialfall ist.
Es geht aber noch weiter: Wenn Sie in der Definition mit der rechten Maustaste auf den Eintrag ArithmeticException klicken und dann wiederum den Kontextmenü-Eintrag Definition einsehen auswählen, erhalten Sie die Definition der Klasse ArithmeticException und mit SystemException die übergeordnete Ausnahmeklasse. Dass die Hierarchie einigermaßen flach ist, erkennen Sie, wenn Sie auch noch die übergeordnete Klasse von SystemException ermitteln – hier landen Sie dann nämlich bei der Klasse Exception, die keine weitere übergeordnete Ausnahmeklasse besitzt.
Exception mit oder ohne Variable?
In allen bisherigen Beispielen haben wir das Exception-Objekt in einer Variablen namens e gespeichert, um innerhalb des catch-Blocks auf die Eigenschaften des Ausnahme-Objekts zugreifen zu können. Diese Variable können Sie auch weglassen, wenn Sie gar nicht auf die Eigenschaften zugreifen wollen. Meist ist dies nicht notwendig – im Falle der DivideByZeroException etwa gibt es in der betroffenen Methode ja nur eine Zeile, die diesen Fehler auslösen kann, und der Grund dafür ist dann auch offensichtlich. Sie können e also auch einfach weglassen:
... catch (DivideByZeroException) { Console.WriteLine("Der Dividend darf nicht 0 sein."); } ...
In diesem Fall erscheinen dann auch keine Warnungen wie Die Variable “e” ist deklariert, wird aber nie verwendet. mehr in der Fehlerliste von Visual Studio.
Ausnahme in übergeordneter Methode verarbeiten
Manchmal möchten Sie Fehler direkt an Ort und Stelle verarbeiten, also beispielsweise in der Methode, in der die Ausnahme ausgelöst wurde. Es kann jedoch auch Fälle geben, wo die Ausnahme in einer übergeordneten Methode behandelt werden soll – also in der Methode, von der aus die untergeordnete Methode aufgerufen wurde.
Ein Beispiel finden Sie in Listing 5. Hier ruft die Methode FehlerbehandlungUebergeordnet die Methode FehlerbehandlungUntergeordnet auf, packt aber diesen Aufruf in einen try-Block. Was geschieht nun? Obwohl die Methode FehlerbehandlungUntergeordnet keine eigene Behandlung von Ausnahmen enthält, wird der Fehler aufgrund der Fehlerbehandlung in der übergeordneten Fehlerbehandlung behandelt.
public static void FehlerbehandlungUebergeordnet() { try { FehlerbehandlungUntergeordnet(); } catch (Exception e) { Console.WriteLine("Es ist ein Fehler aufgetreten: {0}", e.GetType().ToString()); } } private static void FehlerbehandlungUntergeordnet() { decimal Null = 0; decimal Quotient = 1 / Null; Console.WriteLine("Der Quotient lautet: {0}", Quotient); Console.ReadLine(); }
Listing 5: Fehler in untergeordneter Methode behandeln