Exception Handling, Tracing und Logging C#.NET Daniel Malovetz Inhaltsverzeichnis 1 Ausnahmen in.net: Exceptions 2 1.1 Definition von Ausnahmen......................... 2 1.2 Ausnahmebehandlung............................ 2 1.2.1 Fangen von Ausnahmen...................... 2 1.2.2 Richtlinien zur Ausnahmenbehandlung.............. 3 1.3 Hierarchie von Ausnahmen......................... 4 1.3.1 Die Klasse Exception........................ 4 1.3.2 SystemException.......................... 4 1.3.3 ApplicationException....................... 4 1.4 finally-statement.............................. 5 1.5 Auslösen von Ausnahmen......................... 5 1.6 Unterschiede zu Java............................ 6 1.7 Eigene Exceptions............................. 6 1.8 Motivation für den Einsatz von Ausnahmen................ 6 2 Tracing und Logging 7 2.1 Logging................................... 7 2.2 Die Klassen Debug und Trace....................... 7 2.3 Debug: wichtige Funktionen........................ 8 2.3.1 TraceListener............................ 9 2.3.2 TextWriterTraceListener...................... 10
1 Ausnahmen in.net: Exceptions 1.1 Definition von Ausnahmen Ausnahmen sind Hilfsmittel, die dazu dienen, Informationen über bestimmte Programmzustände, meistens Fehlerzustände, zur Behandlung an andere Programmebenen weiterzureichen[unka]. Unbehandelte Ausnahmen brechen das Programm ab. Es gibt verschiedene Arten von Ausnahmen, die bei verschiedenen Arten von Fehlern auftreten (geworfen werden). Es besteht die Möglichkeit, geworfene Ausnahmen zu behandeln (fangen) um den Programmablauf fortzusetzen. 1.2 Ausnahmebehandlung 1.2.1 Fangen von Ausnahmen Um zu verhindern, dass ein Programm abbricht, in dessem Ablauf eine Ausnahme aufgetreten ist, gibt es die Möglichkeit Ausnahmen zu fangen 1. C# stellt hierfür try und catch Ausdrücke zur Verfügung. Kritische Code Bereiche, in denen Ausnahmen auftreten können, werden in den try{ Block geschrieben. Die CLR 2 prüft bei der Ausführung dieser kritischen Bereiche, ob eine Ausnahme aufgetreten ist. Nachfolgend gibt es für den Programmierer die Möglichkeit, bestimmte Ausnahmen per catch(exception e){ zu fangen und Programmcode auszuführen, der den Fehler behandelt. Tritt ein Ausnahme in einem try-block auf, dann durchsucht die Laufzeitumgebung die zugehörigen catch- Blöcke nach einem Behandler für den Typ der aufgetretenen Ausnahme. Gibt es einen catch-block, der den Typ der aufgetretenen Ausnahme oder einen Typ, von dem die Ausnahme abgeleitet ist, behandelt, dann beendet das System die Suche. Wenn kein geeigneter catch-block gefunden wird und der try-block, in dem die Ausnahme aufgetreten ist, in einem anderen try-block verschachtelt ist, dann wird die Ausnahme an die höher gelegene Ebene des Aufrufstapels weitergegeben und es wird dort nach einem passenden Behandler gesucht. Dieses Verfahren wird fortgeführt, bis die Ausnahme behandelt wurde oder es keine höhergelegene Aufrufebene des Aufrufstapels mehr gibt. Wenn letzteres der Fall ist, dann wird die Anwendung beendet.[unke] Ein Beispiel für eine so behandelte Ausnahme wäre: 1 engl. to catch 2 Common Language Runtime
int[] array = new int[] {1,2,3,4; public int Foo(){ try{ return array[4]; catch(indexoutofrangeexception e1){ Console.WriteLine("Ausnahme aufgetreten"); catch(exception e2){... In diesem Beispiel wird ein array instanziert. Die Methode Foo versucht nun, auf einen Index zuzugreifen, der außerhalb des Arrays liegt. Alle Codebereiche, die Ausnahmen auslösen könnten, werden in einem try-block ausgeführt. Die Laufzeitumgebung erkennt den Fehler und wirft eine IndexOutOfRangeException, welche jedoch im nachfolgenden catch-block gefangen wird. Um anzuzeigen, dass eine Ausnahme aufgetreten ist und gefangen wurde, wird zusätzlich ein Text auf die Konsole geschrieben. In diesem Beispiel ist auch zu sehen, dass mehrere catch-blöcke hintereinander geschrieben werden können. Somit kann auf verschiedene Typen von Ausnahmen, die auftreten könnten, individuell reagiert werden. Die Blöcke werden der Reihe nach abgearbeitet, eine bereits gefangene Ausnahme kann nicht nochmal behandelt werden. 1.2.2 Richtlinien zur Ausnahmenbehandlung Da das Auftreten einer Ausnahme auf schwerwiegende Fehler und unerwartete Zustände im Programmablauf hinweist, ist es nicht sinnvoll, pauschal alle Ausnahmen zu fangen. Ausnahmen sollten aufgefangen werden, wenn der Grund bekannt ist, warum sie aufgetreten sind und wenn es eine sinnvolle Möglichkeit gibt, den Fehler zu umgehen.[unkc] Vor Allem sollten Ausnahmen nicht verwendet werden, um den Programmablauf zu steuern. Oft ist es auch nötig, einen Benutzer von einer gefangenen Ausnahme in Kenntnis zu setzen. Ein Beispiel wäre eine Überweisung in einer Bank Software, die aufgrund einer aufgetretenen Ausnahme nicht ausgeführt werden konnte. Ein Beispiel für eine Ausnahme, die gefangen werden kann ist eine FileNotFound- Exception, die auftritt, wenn der Benutzer eines Programms auf eine nicht vorhandene Datei zugreifen will. Dem Nutzer kann dann beispielsweise durch die Ausnahmebehandlung ermöglicht werden, auf eine andere Datei zuzugreifen.[unkc] Einige Ausnahmen können überhaupt nicht gefangen werden. Zu diesem Typ gehören:
StackOverflowException ThreadAbortException ThreadInterruptException ExecutionEngineException OutOfMemoryException 1.3 Hierarchie von Ausnahmen Ausnahmen sind Objekte und damit vom Typ Object abgeleitet, die Grundklasse aller Ausnahmen ist die Klasse Exception. Von dieser Klasse aus spalten sich mehrere Unterklassen von Ausnahmen ab. Die zwei wichtigsten Unterklassen sind SystemException und ApplicationException. 1.3.1 Die Klasse Exception Instanzen der Ausnahmen-Basisklasse Exception werden in der Regel nicht geworfen. Die Grundlegenden Eigenschaften, welche die Klasse an alle anderen Ausnahmen vererbt sind StackTrace, Source und Message. StackTrace verfolgt alle Methoden, die im Moment ausgeführt werden. Mit Hilfe dieser Eigenschaft lassen sich Ausnahmen bis zu der Zeile einer Methode verfolgen, in der sie aufgetreten sind. Source enthält den Namen des Objekts oder der Anwendung, die eine Ausnahme ausgelöst hat. Message enthält eine Meldung, welche beim Erstellen der Ausnahme zu ihrer Beschreibung übergeben wird. Die Beschreibung sollte nach Möglichkeit vollständig sein und Möglichkeiten bieten, wie der Fehler vermieden werden kann.[unke] 1.3.2 SystemException Die Ausnahmen dieser Klasse werden von der CLR ausgelöst, wenn Laufzeitprüfungen, wie der Zugriff auf ein Array im definierten Bereich, fehlschlagen. Ausnahmen, die von dieser Klasse erben werden im Normalfall nicht abgefangen oder selbst geworfen, da sie auf Programmierfehler hinweisen[unkd]. Um diese Ausnahmen zu verhindern muss der Programmcode überarbeitet werden. 1.3.3 ApplicationException Ausnahmen dieser Klasse werden vom Benutzerprogramm ausgelöst, nicht von der CLR. Eigene definierte Ausnahmen sollten von dieser oder einer ihrer Unterklassen abgeleitet sein. Für die meisten Fehlerfälle gibt es bereits vom Framework vordefinierte Ausnahmen, welche auch benutzt werden sollten. Bereits vorhandene Ausnahmen sollten nicht neu geschrieben werden. Eigene Ausnahmen sollten nur definiert werden, wenn ein Fehler
anwendungsspezifisch behandelt werden muss. Ein mögliches Beispiel wäre eine Ausnahme, die das Fehlen einer bestimmten, für die Fortsetzung der Anwendung notwendigen Ressource aufzeigt 1.4 finally-statement Unabhängig davon, ob in einem kritischen Bereich eine Ausnahme aufgetreten ist oder nicht, ist es manchmal wichtig, dass bestimmter Programmcode in jedem Fall ausgeführt wird. Bei einem Zugriff auf eine Datenbank muss eine geöffnete Verbindung zu dieser auch nach einer aufgetretenen Ausnahme wieder geschlossen werden, bei einem Zugriff auf begrenzte Ressourcen müssen diese auch trotz Programmabbruch freigegeben werden. Dieser Code zum Aufräumen kann in einen finally Block geschrieben werden, der auf den try Block folgt. Der Code der in diesem Block steht wird in jedem Fall ausgeführt, unabhängig davon, ob eine Ausnahme auftritt oder nicht. try{ //kritischer Programmteil finally{ //Programmteil wird in jedem Fall ausgeführt 1.5 Auslösen von Ausnahmen Um selbst im Programm eine Ausnahme zu werfen, kann der Ausdruck throw verwendet werden. Damit lassen sich auch bereits gefangene Ausnahmen weiter nach Oben in der Aufrufebene werfen, um ihnen z.b. mehr Informationen für das Debugging beizufügen. Um die Aufrufliste (StackTrace) nicht zu verfälschen wirft man dazu meist eine leere Ausnahme mit dem Befehl throw; ohne Parameter. Eine weitere Möglichkeit der Weitergabe einer gefangenen Ausnahme besteht darin, die gefangene Ausnahme als inner Parameter des Konstruktors für eine neue Ausnahme zu verwenden. Die alte, gefangene Ausnahme wird dann an die Neue angehängt, damit lassen sich geschachtelte Ausnahmen erzeugen. Wenn man jedoch die gefangene Ausnahme, z.b. Exception e selbst nochmal wirft, so gilt dies als neues Auftreten einer Ausnahme und der gesamte bisher gesammelte StackTrace geht verloren.[unkb]
1.6 Unterschiede zu Java Die Programmiersprache Java verlangt vom Programmierer im Unterschied zu C#, dass er bestimmte Ausnahmen behandelt. Diese sogenannten checked Exceptions müssen dann entweder gleich in den Methoden abgefangen werden, in denen sie auftreten oder mit dem Schlüsselwort throws in der Aufrufliste weitergegeben werden. Das verleitet dazu, dass checked Exceptions die auftreten könnten überall mit dem Zusatz throws Exception weitergegeben oder einfach leere catch Blöcke angehängt werden, um das Problem zeitlich nach hinten zu verlagern, was bei großen Systemen zu extrem unübersichtlichem Programmcode führt. Dieses Schlüsselwort gibt es in C# nicht. 1.7 Eigene Exceptions In C# und.net gibt es die die Möglichkeit, eigene Ausnahmen zu definieren. class MyException : ApplicationException{ MyException() { MyException(string message) : base(message) { MyException(string message, Exception inner) : base(message, inner) { In den Konstruktoren für diese neue Klasse von Ausnahmen werden die Konstruktoren der Basisklasse aufgerufen. Der erste Konstruktor ohne Parameter wirft eine einfache Ausnahme, der zweite Konstruktor hängt an die Ausnahme noch eine Nachricht an, die weitere Fehlerbeschreibungen enthält. Der letzte Konstruktor bietet die Möglichkeit, eine vorher bereits gefangene Ausnahme mit dem Parameter inner an die neue Ausnahme anzuhängen.[hag03] 1.8 Motivation für den Einsatz von Ausnahmen Wenn eine Methode in einem Programm einen Ausnahmezustand herbeiführt, könnte man beispielsweise auch vorher vereinbarte spezielle Rückgabewerte zurückgeben, anstatt Ausnahmen zu werfen. Eine mögliche Rückgabe dieser Art wäre beispielsweise: if(nenner == 0) return -1; Um auszuwerten, ob die aufgerufene Methode einen Fehler ausgelöst hat, kann man meist nur lange case Statements verwenden, die alle möglichen Fehlerrückgabewerte abprüft. Methoden haben jedoch unterschiedliche Rückgabewerte, einige Methoden haben sogar keinen Rückgabewert (void). Auch ist es möglich, dass bestimmte Rückgabewerte, die
Fehler anzeigen sollten, übersehen werden, wenn eine Methode aufgerufen wird.[unkf] Ausnahmen sind dagegen per Namen selbsterklärend und können nicht übersehen werden. Das obige Beispiel liese sich wie folgt durch eine DivideByZeroException regeln: if(nenner == 0) throw new DivideByZeroException(); 2 Tracing und Logging 2.1 Logging Um die Entwicklung eines Programms zu unterstützen ist es wichtig, dass auch zur Laufzeit Informationen über den Ablauf des Systems gesammelt werden, um beispielsweise den Inhalt von Variablen oder die Korrektheit von Berechnungen zu prüfen. Das Sammeln dieser Informationen wird als Tracing und Logging 3 bezeichnet. Ein Ansatz zum Sammeln dieser Informationen wäre, sie auf die Konsole auszugeben mit der Console.- WriteLine() Methode. int value = 42; Console.WriteLine("Wert von value = {0", value); Dieser Ansatz birgt jedoch einige Probleme. Die Ausgabe auf der Konsole wird unübersichtlich, die Informationen zur Ablaufverfolgung mischen sich mit den funktionalen Ausgaben des Programms und diese können nicht leicht voneinander unterschieden werden. Die nicht funktionalen Informationen die auf die Konsole geschrieben wurden müssen vor dem Ausliefern des fertigen Programms an den Kunden wieder gelöscht werden damit Leistung und Codeumfang des Endprodukts nicht beeinflusst werden. Das kostet Zeit und das ausgelieferte Programm entspricht damit auch nicht dem Getesteten. Auch verwenden viele Anwendungen keine Konsole für Ausgaben.[Küh08] Das.NET Framework stellt für die Lösung des Problems zwei Klassen zur Verfügung, Debug und Trace. 2.2 Die Klassen Debug und Trace Die Debug Klasse, die im Namespace System.Diagnostics liegt, bietet einen großen Funktionsumfang um Meldungen zu erzeugen, die der Überwachung der Programmausführungssequenz dienen. Diese Ausgaben erscheinen nicht auf der Konsole, sondern standardmäßig im Ausgabe Fenster der Entwicklungsumgebung 4. C# bietet die Möglichkeit, Programmcode in zwei verschiedenen Modi zu kompilieren, dem Debug und dem Release Modus. Während alle Ausgaben der Debug Klasse angezeigt werden, wenn das Programm im Debug Modus kompiliert wird, tauchen diese Anzeigen 3 dt. Ablaufverfolgung und Daten erfassen 4 Die Begründung hierfür findet sich im Abschnitt TraceListener
im fertigen Release Code nicht mehr auf. Der Programmcode muss vor der Auslieferung an den Kunden nicht von Debug Informationen bereinigt werden und es entstehen auch keine Leistungseinbußen durch das Abarbeiten von großen Mengen von Debug Code. Eine weitere Klasse, die die Arbeit des Loggings erleichtert ist die Klasse Trace. Die Klassen Debug und Trace unterscheiden sich in ihren Methoden und Eigenschaften, Methoden wie Write und Assert, aber auch die Listeners, können genauso durch die Trace Klasse benutzt werden. Ferner teilen sich Debug und Trace eine ListenerCollection, was zur Auswirkung hat, dass ein Listener der durch eine der beiden Klassen zur Sammlung hinzugefügt wurde, die Ausgabeinformationen beider Klassen gleichzeitig aufzeichnet. Der Unterschied der beiden Klassen liegt darin, dass Anweisungen der Trace Klasse auch im Release Modus des C# Compilers mit übersetzt werden. Damit kann man alle Logging Funktionen auch im fertigen Release Code verwenden um beispielsweise Daten für nachträgliche Updates zu sammeln.[küh08] 2.3 Debug: wichtige Funktionen Die wichtigsten Methoden und Funktionen der Debug Klasse sind Write, WriteLine WriteIf, WriteLineIf Assert Indent, Unindent Listeners Die Methoden Write und WriteLine können fast genauso verwendet werden, wie die gleichnamigen Methoden der Konsole. Der Unterschied liegt darin, dass die Methoden der Debug Klasse keine Formatierungsmöglichkeiten unterstützen. Mehrere Informationen in einer gemeinsamen Zeichenfolge müssen also mit dem Verknüpfungsoperator + zusammengefügt werden. Debug.Write und Debug.WriteLine sind mehrfach überladen und akzeptieren Argumente vom Typ string und object. Optional besteht auch die Möglichkeit, ein zweites string-argument zu übergeben, das eine detaillierte Beschreibung der ausgegebenen Information enthält, welche vor der eigentlichen Debug- Information ausgegeben wird. int value = 42; Debug.WriteLine("Wert von value = " +value, "Ablaufverfolgung"); Dieses Beispiel würde folgende Ausgabe produzieren: Ablaufverfolgung: Wert von value = 42
Eine Überladung der beiden Methoden ohne Argumente 5 existiert nicht. WriteIf und WriteLineIf akzeptieren als erstes Argument einen boolean Wert und als zweites einen string. Die Ausgabe des string erfolgt nur, wenn der boolean zu true ausgewertet wird. Mit der Assert Methode lassen sich Assertionen 6 in den Programmcode einfügen. Die Methode nimmt einen boolean Wert und einen string. Wenn der Wert zu false ausgewertet wird, dann pausiert das Programm an dieser Stelle und ein Assertionsfehler wird angezeigt mit dem string als Nachricht. Ein Ziel der Ablaufverfolgung ist es, die Informationen übersichtlich zu halten um sie leicht auswertbar zu machen.[küh08] Hierfür stehen innerhalb der Debug Klasse die Methoden Indent() und Unindent() zur Verfügung. Mit Indent() werden alle nachfolgenden Ausgaben der Debug Klasse eingerückt, die Methode Unindent() rückt alle nachfolgenden Ausgaben wieder heraus. Über die Eigenschaft IndentSize lässt sich die Einzugsebene 7 beliebig verändern. Indent() lässt sich auch mehrmals anwenden um verschiedene Einrückebenen zu erzeugen. 2.3.1 TraceListener Standardmäßig erfolgt die Ausgabe der Debug und Trace Klassen über das Ausgabefenster von Visual Studio. Der Grund dafür ist, dass standardmäßig ein ConsoleTrace- Listener in der ListenerCollection eingetragen ist. Dies ist für hohe Summen von geloggten Informationen schlecht auswertbar und die Informationen gehen verloren, sobald die Anwendung beendet wird. Um die Daten übersichtlicher zu gestalten und etwa in einer Datei oder im Windows Ereignisprotokol zu archivieren, können Listener-Objekte verwendet werden. Im Namespace System.Diagnostics befinden sich fünf vordefinierte Listener, die von der Klasse TraceListener abgeleitet sind. ConsoleTraceListener DefaultTraceListener DelimetedListTraceListener TextWriterTraceListener EventLogTraceListener 5 also Debug.Write(); 6 Zusicherungen 7 standardmäßig 4 Zeichen
2.3.2 TextWriterTraceListener Mit dieser Klasse können gesammelte Daten in.txt Dateien gespeichert werden. Ein Objekt dieser Klasse wird mit der zu beschreibenden Datei instanziiert und wird in die Listener- Collection aufgenommen. Diese Sammlung verwaltet alle Listener Objekte. Alle gesammelten Daten werden jetzt in die übergebene.txt Datei geschrieben. Debug.Listeners.Clear(); TextWriterTraceListener listener; listener = new TextWriterTraceListener(@"C:\Protocol.txt"); Debug.Listeners.Add(listener); Debug.WriteLine("Debugging Information begin"); Debug.Indent(); Debug.WriteLine("...Information..."); Debug.Unindent(); Debug.WriteLine("Debugging Information end"); listener.flush(); listener.close(); Die Methode Clear() löscht die bereits vorhandenen Einträge der ListenerCollection. Add() fügt den neuen Listener der Sammlung hinzu. Flush() schreibt den Inhalt des Puffers des Listeners in die Datei und löscht den Puffer. Close() schließt das Listener Objekt letztlich wieder und löscht es aus der Sammlung.[Küh08]
Literatur [Bar] Erik Bartmann. C# Debugging. [Hag03] Christoph Hager. C# - Das Grundlagenbuch. Data Becker, 2003. [Küh08] Andreas Kühnel. Visual C# 2008. Galileo Computing, 2008. [Thi03] [unka] [unkb] [unkc] [unkd] [unke] [unkf] Christian Thilmany..NET Patterns: Architecture, Design, and Process. Addison-Wesley Professional, 2003. unknown. http://de.wikipedia.org/wiki/ausnahmebehandlung. unknown. http://msdn.microsoft.com/de-de/library/6dekhbbc unknown. http://msdn.microsoft.com/de-de/library/ms229005.aspx. unknown. http://msdn.microsoft.com/de-de/library/ms229005.aspx. unknown. http://msdn.microsoft.com/de-de/library/system.exception.aspx. unknown. http://www.cachaca.de/index.php?section=108.