Advanced Services | try catch finally |
Ausnahmen werden grundsätzlich mit einem try-catch-Konstrukt abgefangen. Es gibt jedoch Varianten, wann man einen Fehler abfangt. Eine Möglichkeit ist, einen Fehler sofort abfangen, wenn er auftritt. Es kann jedoch sein, daß ein Entwickler eine Methode einer Klasse schreibt, einen Fehler absichtlich nicht abfängt und so einem anderen Programmierer, der die Klasse verwendet, die Möglichkeit gibt, selbst und individuell auf die Ausnahme zu reagieren. Diese Situation haben wir ja ständig wenn wir Methoden aus der API verwenden. Wir behandeln zunächst den ersten Fall und fangen den Fehler direkt ab. Dies geschieht mit einem try-catch Konstrukt oder in erweiterter Form mit einem try-catch-finally Konstrukt. Die Struktur des Konstrukts ist die folgende:
try // Versuchsblock { // statements vor dem kritischen statement int zahl = Convert.ToInt32(usereingabe); // statements nach dem kritischen statement } catch(Exception ex) // Fehlerbehandlungsblock { Console.WriteLine("Das ist nicht gut gegangen"); } //nächste Anweisung
Die Klammern sind in beiden Blöcken obligatorisch. Der Versuchsblock wird bis zum kritischen Statement abgearbeitet. Erzeugt das kritische Statement keinen Fehler, so wird es abgearbeitet und die nächsten Statements werden abgearbeitet bis zu einem weiteren kritischen Statement oder bis zum Ende des Versuchsblocks. In diesem Fall wird der catch-Block übersprungen und es folgt die nächste Anweisung nach dem catch-Block. Scheitert ein kritischer Aufruf dagegen, so werden die Statements nach dem kritischen Aufruf nicht mehr abgearbeitet, sondern sofort der catch-Block angesprungen und seine Anweisungen abgearbeitet. Wie wir gesehen haben sind die Exceptions hierarchisch geordnet und die Klasse Exception ist die Oberklasse für alle Exceptions. Mit der obigen Konstruktion hat man so auf alle Fälle alle Fehlersituationen im Programm abgefangen.
Zu einem try-Block kann es durchaus mehrere catch-Blöcke geben. Es kommt lediglich darauf an wie differenziert man die Fehlerbehandlung durchführen will. Bereits die oben verwendete Methode kann zwei Exceptions werfen. Wir modifizieren das obige Beispiel ein wenig und schreiben für jede Exception einen eigenen FehlerBlock. So haben wir die Möglichkeit jeden Fehler spezifisch zu behandeln.
static void Main() { try { //String st = "keine Zahl"; String st = "77777"; // zahl zu groß ushort zahl = Convert.ToUInt16(st); Console.WriteLine(zahl); } catch (FormatException ex) { Console.WriteLine(ex.Message); //Console.WriteLine(ex.ToString()); } catch (OverflowException ex) { Console.WriteLine(ex.Message); //Console.WriteLine(ex.ToString()); } Console.WriteLine("nach catch"); }
Mit den beiden catch-Blöcken haben wir alle Situationen von Convert.ToUInt16(st) erfaßt. Will man keine so ausführliche Fehlerbehandlung, so nimmt man die erste gemeinsame Oberklasse der zwei Exceptions und dann reicht ein einziger catch-Block. Aus der API erfahren wir: FormatException ist eine Unterklasse von SystemException, OverflowException ist eine Unterklasse von ArithmeticException, diese wiederum ist eine Unterklasse von SystemException. SystemException selbst ist eine Unterklasse von Exception. Um Verwechslungen zu vermeiden seien hier die beiden Exceptions nochmal mit ihrem vollen Namen genannt: System.SystemException ist eine Unterklasse von System.Exception!
static void Main() { try { //String st = "keine Zahl"; String st = "77777"; // zahl zu groß ushort zahl = Convert.ToUInt16(st); Console.WriteLine(zahl); } catch (SystemException ex) { Console.WriteLine(ex.Message); //Console.WriteLine(ex.ToString()); } Console.WriteLine("nach catch"); }
Die Reihenfolge bei mehreren catch-Blöcken ist nicht immer beliebig. Stehen die Exceptions in einer direkten Vererbungslinie, so muß zuerst die spezielle Ausnahme und dann die allgemeinere Ausnahme abgefangen werden. Übersieht man das, serviert uns der Compiler eine Fehlermeldung. In der folgenden Variante unseres Beispiels ist die Reihenfolge der catch-Blöcke falsch, da OverflowException eine Unterklasse von SystemException ist.
try { //String st = "keine Zahl"; String st = "77777"; ushort zahl = Convert.ToUInt16(st); Console.WriteLine(zahl); } catch (SystemException ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.ToString()); } catch (OverflowException ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.ToString()); } Console.WriteLine("nach catch"); // Fehlermeldung: A previous catch clause already catches all exceptions of this or of a super type
Man kann das try-catch Konstrukt durch einen finally-Block ergänzen. Falls dieser optionale Block vorhanden ist, so wird er auf jeden Fall abgearbeitet, d.h. er kommt entweder direkt nach try oder nach dem letzten catch. Der finally-Block wird in der Regel dazu verwendet um irgendwelche Aufräumarbeiten zu erledigen, etwa das Schließen von Dateien.
try { // Versuchsblock } catch(InterruptedException ex) { // Fehlerbehandlungsblock } finally { // Aufräumblock } //nächste Anweisung
static void Main() { DriveInfo di = new DriveInfo("x:\\"); Console.WriteLine(di.AvailableFreeSpace); }
Die drei Exceptions ArgumentException, ArgumentNullException und DriveNotFoundException haben SystemException als gemeinsame Oberklasse. Es bietet sich also folgendes Konstrukt an.
static void Main() { try { DriveInfo di = new DriveInfo("x:\\"); Console.WriteLine(di.AvailableFreeSpace); } catch(SystemException ex) { // Fehlerbehandlung } }
Es kann nicht schaden vorsichtigerweise statt SystemException gleich Exception zu nehmen, dann ist man immer auf der sicheren Seite!
Typische Laufzeitfehler wie NullReferenceException und IndexOutOfRangeException deuten in der Regel auf Programmfehler hin. Sie sollten daher nicht abgefangen werden, sondern die Fehler im Programm beseitigt werden.
Beim Entwerfen von Klassen und Methoden will man häufig die Fehlerbehandlung demjenigen überlassen der die Klasse (und ihre Methoden) verwendet. In solchen Fällen kann man throw verwenden um selbst in einer Methode einen Fehler zu werfen wenn eine bestimmte Bedingung erfüllt (oder nicht erfällt) ist. Ein kleines Beispiel wird dies erläutern.
class Person { //... private int age; public void SetAge(int age) { if (age < 0) throw new ArgumentOutOfRangeException("Age Cannot Be Negative : " + age); this.age = age; } }
Hier sorgt der Entwickler von Person dafür, daß für die Methode SetAge() sichergestellt ist, daß der Datenteil age nicht negativ werden kann, überläßt es aber dem Anwender der Klasse Person, wie er damit umgeht. Sinngemäß etwa so:
static void Main(string[] args) { Person p = new Person(); try { int erg = BenutzerEingabe(); p.SetAge(erg); Console.WriteLine("age {0} set", erg); } catch (Exception ex) { Console.WriteLine(ex.Message); }
Wir schreiben das vorige Beispiel um und codieren es mit einer Property.
class Person { //... private int age; public int Age { set { if (value < 0) throw new ArgumentOutOfRangeException("Age Cannot Be Negative : " + value); this.age = value; } } }
Der folgende Programmschnipsel setzt die Property ein. Bei mehrmaligem Aufruf wird der Zufallsgenerator mal eine negative Zahl liefern und dann wird die Exception auftreten.
static void Main(string[] args) { Person2 p = new Person2(); try { Random rd = new Random(); int erg = rd.Next(-10, 10); p.Age = erg; Console.WriteLine("age {0} set", erg); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
Das klingt zunächst seltsam, es gibt aber sehr wohl Gründe, so zu verfahren. Man betrachte etwa das folgende Beispiel.
class Example { static void Main() { Connect(""); } static void Connect(string st) { try { // Do something // i.e. connect to database // if it not works throw new ArgumentException("wrong argument"); } catch (TimeoutException ex) { // Write exception message in a log file throw; } } }
In diesem Fall schreibt der Entwickler die Fehlermeldung in ein Logfile und wirft dieselbe Exception noch einmal. So wird sichergestellt, daß der Fehler dokumentiert wird.
Eine Exception kann eine weitere auslösen. Dieser Vorgang kann auch mehrmal vorkommen, soda&szkig; es zu ganzen Ketten von Exceptions kommen kann. hat eine Methode (oder Property) eine Exception geworfen, so ist dies immer die letzte Exception in der Kette. Über die get-Property Innerexception wird einem angezeigt, ob es eine tieferliegende Exception gibt, die die letzte Exception verursacht hat. Im folgenden Beispiel lösen wir eine FormatException aus, fangen sie ab, und werfen im catch-Zweig eine ArgumentException, der wir die erste Exception mitgeben.
//FormatException oder OverflowException static ushort StringToUShort(string st) { try { return Convert.ToUInt16(st); // wirft FormatException oder OverflowException } catch (Exception ex) { throw new ArgumentException("falsches Argument", ex); } }
Unser Main sieht nun folgendermaß aus.
class InnerExceptions { static void Main() { try { ushort us = StringToUShort("-123"); Console.WriteLine(us); } catch (Exception ex) { Console.WriteLine(ex.GetType() + ": " + ex.Message); if( ex.InnerException !=null) Console.WriteLine("Inner Exception is " + ex.InnerException.GetType() + " : " + ex.InnerException.Message); else Console.WriteLine("There ist no Inner Exception"); } } static ushort StringToUShort(string st) { try { return Convert.ToUInt16(st); // FormatException oder OverflowException } catch(Exception ex) { throw new ArgumentException("falsches Argument", ex); } } } // end Main class
Hier der Output des Programms:
Ein zur try/catch-Klausel verwandtes Konstrukt ist die using-Klausel (nicht zu verwechseln mit der Verwendung von using zum Import von Namensräumen am Anfang eines Programms). Während aber try/catch für die Ausnahmebehandlung steht verwendet man die using-Klausel für Objekte, die über eine Methode Dispose() verfügen um sicherzustellen, daß diese Methode am Ende der Arbeit mit diesem Objekt aufgerufen wird. Aufgabe dieser Methode ist es, wichtige Aufräumarbeiten zu erledigen, in der API heißt es dazu: "Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources". Die using-Klausel wird meist im Zusammenhang mit Dateioperationen oder Datenbankverbindungen gebraucht. Sie wird im Kapitel Dateihandling vorgestellt.