# 9 Fehlerbehandlung und Debugging 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 anstatt 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. ## 9.1 Die Behandlung von Laufzeitfehlern 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 ein Anwender tätigt und vielleicht gar noch in welcher Reihenfolge, wenn er die grafische Benutzeroberfläche einer Applikation bedient? Welcher Anwender kann nach einem Fehler 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. Seien wir doch einmal ehrlich zu uns selbst: Gibt es ein Software-Haus, 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. Diess sind 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, »Debuggen mit Programmcode«, zuwenden. In diesem Abschnitt wollen wir uns mit der Fehlergattung auseinandersetzen, die dazu führt, dass zur Laufzeit eine Ausnahme ausgelöst wird, und die die verschiedensten Ursachen haben kann: Anwender geben unzulässige Werte ein. Es wird versucht, eine nicht vorhandene Datei zu öffnen. Es wird versucht, eine Division durch null durchzuführen. Beim Zugriff auf eine Objektmethode ist der Bezeichner der Objektvariablen noch nicht initialisiert. Eine Netzwerkverbindung ist instabil. ... Die Liste ist schier endlos lang sein. Aber allen Fehlern ist eines gemeinsam: Sie führen zum Absturz des Programms, wenn der auftretende Fehler nicht behandelt wird. ### 9.1.1 Laufzeitfehler erkennen 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.
    using System;
    using System.IO;
    class Program {
      static void Main(string[] args) {
        StreamReader stream = new StreamReader(@"C:\Text.txt");
        Console.WriteLine(stream.ReadToEnd());
        Console.ReadLine();
        myFile.Close();
      }
    }
Hinweis
Denken Sie bitte daran, in einem Zeichenfolgeliteral, das einen einfachen Backslash enthalten soll, zwei aufeinanderfolgende Backslashes anzugeben. Ein einfacher Backslash, wie er beispielsweise oben in einer Pfadangabe benötigt wird, würde zu einer Fehlinterpretation führen, weil er eine Escape-Sequenz einleitet. Sie können diese Interpretation aber aufheben, indem Sie, wie oben gezeigt, das @-Zeichen voranstellen.
Die Klassenbibliothek des .NET Frameworks bietet zum Öffnen einer Textdatei die Klasse StreamReader im Namespace System.IO an. Einer der Konstruktoren dieser Klasse erwartet den vollständigen Pfad zu der zu öffnenden Datei:
    public StreamReader(string path);
Aus dem Datenstrom können mit Read einzelne Zeichen gelesen werden, mit ReadLine eine komplette Zeile. ReadToEnd hingegen 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. Solange die angegebene Datei existiert, wird die Anwendung fehlerfrei ausgeführt. Wenn Sie dem Konstruktor der Klasse StreamReader allerdings eine Zeichenfolge auf eine nicht vorhandene Datei übergeben, wird die Laufzeit der Anwendung mit einer Fehlermeldung unterbrochen und das Programm danach beendet. 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 bool OpenFile(string strFile) {
      // Anweisungen, die zu einem Fehler führen können
      if(kein Fehler aufgetreten) {
        // Anweisungen
        return true;
      }
      else {
        // Anweisungen
        return false;
      }
    }
Der Aufrufer der fiktiven Methode OpenFile kann den Rückgabewert der Funktion nutzen, um den weiteren Programmablauf an die Situation anzupassen:
    public void Caller() {
      // Anweisungen
      if(OpenFile)
        // Datei-Inhalt anzeigen
      else
        // OpenFile mit anderem Argument aufrufen
    }
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 ohne Mängel. 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 Lösung, 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. C# 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. ### 9.1.2 Die Behandlung von Exceptions Ganz im Sinne der Objektorientierung werden Ausnahmen als Objekte angesehen. Das Grundprinzip lässt sich wie folgt beschreiben: 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. 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. ### 9.1.3 Die »try...catch«-Anweisung Sehen wir uns nun zunächst die Syntax der einfachsten Ausnahmebehandlung an:
    try {
      // Anweisungen
    }
    catch(Ausnahmetyp) {
      // Anweisungen
    }
    // Anweisungen
Der try-Block 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 dem catch-Block 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 in 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 catch-Anweisungsblock 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, dass das Programm unkontrolliert beendet wird. Wir wollen diese Ausführungen nun mit einem praktischen Beispiel testen. Dazu greifen wir wieder auf das Beispiel aus Abschnitt 9.1.1, »Laufzeitfehler erkennen«, zurück, in dem eine Datei geöffnet und an der Konsole ausgegeben werden soll.
    // -----------------------------------------------------
    // Beispiel: ...\Kapitel 9\TryCatchSample_1
    // -----------------------------------------------------
    class Program {
      static void Main(string[] args) {
        StreamReader stream = null;
        Console.Write("Welche Datei soll geöffnet werden? ... ");
        string path = Console.ReadLine();
        // Fehlerbehandlung einleiten
        try {
          // Die folgende Anweisung kann zu einer Exception führen.
          stream = new StreamReader(path);
          Console.WriteLine("--- Dateianfang ---");
          Console.WriteLine(stream.ReadToEnd());
          Console.WriteLine("--- Dateiende -----");
          stream.Close();
        }
        catch(FileNotFoundException e) {
           // Falls die angegebene Datei nicht existiert,
           // eine fehlerspezifische Meldung ausgeben.
           Console.WriteLine("Datei nicht gefunden.");
         }
         Console.WriteLine("Nach der Exception-Behandlung");
         Console.ReadLine();
      }
    }
Starten Sie das Programm, und geben Sie nach der Aufforderung einen gültigen Zugriffspfad auf eine Datei an, zum Beispiel:
    C:\MeineProgramme\Lebenslauf.txt
Die Datei wird geöffnet, und ihr Inhalt wird an der Konsole angezeigt. Das Programm wird bis zum catch-Statement ausgeführt und verzweigt danach zu der Anweisung, die dem catch-Block 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. 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:
    stream = new StreamReader(str);
Entscheidend ist, dass eine Anweisung, die einen Laufzeitfehler verursachen könnte, innerhalb des try-Blocks codiert ist, damit sie durch die Fehlerbehandlungsroutine überwacht 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 ausgeführt. In unserem Beispiel wird eine Ausnahme aufgrund einer falschen Dateiangabe behandelt, die durch ein Objekt vom Typ FileNotFoundException beschrieben wird. 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. ### 9.1.4 Behandlung mehrerer Exceptions Das Beispiel oben ist noch sehr naiv codiert, denn der Versuch, eine Datei zu öffnen, 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 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 in diesem Fall nicht durch FileNotFoundException beschrieben wird. Vielleicht haben Sie sich vorhin die Frage gestellt, woher ich weiß, dass eine FileNotFoundException beim Aufruf des Konstruktors der Klasse StreamReader ausgelöst werden kann? Die Antwort ist sehr einfach: Die Angaben sind in der Dokumentation zur .NET-Klassenbibliothek zu finden. Ein Blick in die Dokumentation des in unserem Beispiel eingesetzten StreamReader-Konstruktors verrät, dass dieser sogar insgesamt fünf unterschiedliche Ausnahmen auslösen kann: ArgumentException ArgumentNullException FileNotFoundException DirectoryNotFoundException 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:
    string str;
    StreamReader dataStream = new StreamReader(str);
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 grundsätzlich mehrere 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 auf einen bestimmten Ausnahmetyp reagiert.
    // -----------------------------------------------------
    // Beispiel: ...\Kapitel 9\TryCatchSample_2
    // -----------------------------------------------------
    class Program {
      static void Main(string[] args) {
        StreamReader stream = null;
        Console.Write("Welche Datei soll geöffnet werden? ... ");
        string path = Console.ReadLine();
        // Fehlerbehandlung einleiten
        try {
          // Die folgende Anweisung kann zu einer Exception führen.
          stream = new StreamReader(path);
          Console.WriteLine("--- Dateianfang ---");
          Console.WriteLine(stream.ReadToEnd());
          Console.WriteLine("--- Dateiende -----");
          stream.Close();
        }
        catch(FileNotFoundException e) {
          // Falls die angegebene Datei nicht existiert,
          // eine fehlerspezifische Meldung ausgeben.
          Console.WriteLine("Datei nicht gefunden.");
        }
        catch(ArgumentException e) {
          // falls der Anwender einen Leerstring übergibt
          Console.WriteLine("Sie müssen eine Datei angeben.");
        }
        catch(DirectoryNotFoundException e) {
          // falls das Verzeichnis nicht existiert
          Console.WriteLine("Der Ordner existiert nicht.");
        }
        Console.WriteLine("Nach der Exception-Behandlung");
        Console.ReadLine();
      }
    }
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 Leerstring ü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 später in Abschnitt 9.1.8 noch eingehen. ### 9.1.5 Die »finally«-Anweisung 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(FirstException e) {
      // Anweisungen
    }
    ...
    finally {
      // 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: 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. 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 Laufzeitumgebung 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. 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 und 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, zum Beispiel:
    ...
    catch(XyzException e) {
      // Anweisungen
      return;
    }
In diesem Fall ist return aber nicht von der durchschlagenden Konsequenz, wie wir dieses Statement bisher kennengelernt 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. ### 9.1.6 Das Weiterleiten von Ausnahmen 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: Die Ausnahme wird in der aufgerufenen Methode mit try...catch behandelt. 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 auftritt, das heißt, man kann auch auf try...catch verzichten. Nehmen wir an, die Methode ProcedureA ruft die Methode ProcedureB auf, diese wiederum ProcedureC, 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 ProcedureB, weitergereicht. Wird auch hier nicht auf die Exception reagiert, wird diese an ProcedureA weitergereicht, die dann in der Verantwortung steht, sich um die Exception zu kümmern. Wir wollen uns diese Programmiertechnik nun ansehen.
    // -----------------------------------------------------
    // Beispiel: ...\Kapitel 9\TryCatchSample_3
    // -----------------------------------------------------
    class Program {
      static void Main(string[] args) {
        Console.Write("Welche Datei soll geöffnet werden? ... ");
        string path = Console.ReadLine();
        FileData file = new FileData(path);
        try {
          file.GetData();
        }
        catch(FileNotFoundException e) {
          // falls die angegebene Datei nicht existiert
          Console.WriteLine("Die Datei existiert nicht.");
        }
        catch(ArgumentException e) {
          // falls der Anwender einen Leerstring übergibt
          Console.WriteLine("Leerstring übergeben.");
        }
        catch(DirectoryNotFoundException e) {
          // falls das Verzeichnis nicht existiert
          Console.WriteLine("Verzeichnis existiert nicht.");
        }
        Console.ReadLine();
      }
    }
    class FileData {
      private string path;
      private StreamReader stream;
      public FileData(string path) {
        this.path = path;
      }
      public void GetData() {
        stream = new StreamReader(path);
        Console.WriteLine("--- Dateianfang ---");
        Console.WriteLine(stream.ReadToEnd());
        Console.WriteLine("--- Dateiende -----");
        stream.Close();
      }
    }
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 Datei entgegennimmt, die geöffnet werden soll. 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 Methode 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 er 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. 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 TryCatchSample_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\TryCatchSample_4
    // -----------------------------------------------------
    class Program {
      static void Main(string[] args) {
        Console.Write("Welche Datei soll geöffnet werden? ... ");
        string path = Console.ReadLine();
        FileData file = new FileData(path);
        try {
          file.GetData();
        }
        catch(FileNotFoundException e) {
          // falls die angegebene Datei nicht existiert
          Console.WriteLine("Die Datei existiert nicht.");
        }
        catch(ArgumentException e) {
          // falls der Anwender einen Leerstring übergibt
          Console.WriteLine("Leerstring übergeben.");
        }
        catch(DirectoryNotFoundException e) {
          // falls das Verzeichnis nicht existiert
          Console.WriteLine("Verzeichnis existiert nicht.");
        }
        Console.ReadLine();
      }
    }
    class FileData {
      private string path;
      private StreamReader stream;
      // ----- Konstruktor -----
      public FileData(string path) {
        this.path = path;
      }
      // ----- Instanzmethode -----
      public void GetData() {
        try {
          stream = new StreamReader(path);
          Console.WriteLine("--- Dateianfang ---");
          Console.Write(stream.ReadToEnd());
          Console.WriteLine("--- Dateiende ---");
          stream.Close();
        }
        catch(FileNotFoundException e) {
          // falls die angegebene Datei nicht existiert
          throw e;
        }
        catch(ArgumentException e) {
          // falls der Anwender einen Leerstring übergibt
          throw e;
        }
        catch(DirectoryNotFoundException e) {
          // falls das Verzeichnis nicht existiert
          throw e;
        }
      }
    }
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, wobei die Referenz des ausgelösten Exception-Objekts übergeben wird:
    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 reagieren. Grundsätzlich reicht es immer aus, eine passende catch-Klausel anzugeben, auch wenn deren Anweisungsblock leer bleibt:
    catch(FileNotFoundException e) { }
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. #### 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 jetzt spezifische Informationen an den Benutzer zurückgegeben. Dazu wird die ausgelöste Exception in eine andere verpackt. Die in der catch-Klausel erzeugte Ausnahme ist eine äußere Exception, 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\TryCatchSample_5
    // -----------------------------------------------------
    ...
    static void Main(string[] args) {
      ...
      try { ... }
      catch (FileNotFoundException e) {
        // falls die angegebene Datei nicht existiert
        Console.WriteLine(e.Message + "- Die Datei existiert nicht.");
      }
      catch (ArgumentException e) {
        // falls der Anwender einen Leerstring übergibt
        Console.WriteLine(e.Message + "- Die Datei existiert nicht.");
      }
      catch (DirectoryNotFoundException e) {
        // falls das Verzeichnis nicht existiert
        Console.WriteLine(e.Message + "- Die Datei existiert nicht.");
      }
      Console.ReadLine();
    }
In der Klasse FileData:
    public void GetData() {
      try { ... }
      catch(FileNotFoundException e) {
        // falls die angegebene Datei nicht existiert
        throw(new FileNotFoundException("In GetData()", e));
      }
      catch(ArgumentException e) {
        // falls der Anwender einen Leerstring übergibt
        throw(new ArgumentException("In GetData()", e));
      }
      catch(DirectoryNotFoundException e) {
        // falls das Verzeichnis nicht existiert
        throw(new DirectoryNotFoundException("In GetData()",e));
      }
    }
Um die Referenz der Ausnahme an den Aufrufer weiterzuleiten, wird das throw-Statement benutzt. Dieses löst die 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 TryCatchSample_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, wie:
    public <Exceptiontyp>(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. Er muss nur noch die zusätzlich übermittelten Informationen auswerten. Dazu genügt es, die Eigenschaft Message des über den Parameter »e« referenzierten Exception-Objekts abzurufen. ### 9.1.7 Die Hierarchie der Exceptions .NET unterstützt die Anwendungsentwickler durch eine Vielzahl vordefinierter Ausnahmeklassen. Ganz im Sinne eines guten objektorientierten Ansatzes sind alle in einer Klassenhierarchie geordnet. Hinter dieser Strukturierung steckt die Absicht, ausgehend von einer Basisausnahme eine immer weiter gehende Verzweigung zu ermöglichen, was letztendlich zu immer spezialisierteren Ausnahmen führt. 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 die 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. Hier klicken, um das Bild zu vergrößern Abbildung 9.1 Auszug aus der Exception-Hierarchie ### 9.1.8 Die Reihenfolge der »catch«-Klauseln 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:
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 { ... }
    catch(FileNotFoundException e) { ... }
    catch(DirectoryNotFoundException e) { ... }
    catch(ArgumentNullException e) { ... }
    catch(ArgumentException e) { ... }
    catch(Exception e) { ... }
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 Laufzeitumgebung fündig, und die Anweisungen innerhalb dieses Blocks werden abgearbeitet. Nun stellen wir uns vor, ArgumentOutOfRangeException sei 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 Laufzeitumgebung fündig, da die Klasse ArgumentException die Basisklasse der tatsächlich ausgelösten Ausnahme ist (vergleichen Sie dies bitte mit Abbildung 9.1). 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 ausgeführt, um die Ausnahme zu behandeln. 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 catch-Klausel, die in der Klassenhierarchie zumindest die Basisklasse der ausgelösten ist, wird die Ausnahme behandelt. Die Reihenfolge der catch-Zweige ist zwingend vorgegeben. Allerdings müssen Sie nicht die gesamte Vererbungshierarchie auswendig lernen, um die vorgeschriebene Reihenfolge einzuhalten. Darauf achtet nämlich bereits Visual Studio, und es wird Ihnen melden, wenn Sie zum Beispiel versuchen, erst eine IOException und anschließend eine FileNotFoundException zu behandeln. Theoretisch wäre es natürlich möglich und es 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. Dann ist es 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. ### 9.1.9 Die Basisklasse »Exception« 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 mit der aktuellen Aufrufreihenfolge aller Methoden zurück.
TargetSite | Liefert die Methode zurück, in der die Ausnahme ausgelöst worden ist.
### 9.1.10 Benutzerdefinierte Ausnahmen 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. Wir wollen nun eine benutzerdefinierte Ausnahme an einem Beispiel entwickeln. Dazu benutzen wir noch einmal unser Beispiel der Circle-Klasse des Projekts GeometricObjects und schauen uns hier exemplarisch den einfach parametrisierten Konstruktor an:
    public Circle(double radius) : this() {
      Radius = radius;
    }
Wie Sie sich sicherlich noch erinnern, hatten wir festgelegt, dass die Übergabe an die Eigenschaft Radius größer oder gleich 0 sein muss. Die Eigenschaftsmethode Radius der Klasse löst ein Ereignis aus, wenn versucht wird, dem Radius einen negativen Wert zuzuweisen. Das ist auch so weit gut, hat aber einen gravierenden Nachteil im Zusammenhang mit den Konstruktoren. Betrachten Sie dazu ein Codefragment, in dem ein Ereignishandler bei dem Ereignis registriert wird:
    Circle kreis = new Circle();
    kreis.InvalidRadius += InvalidRadiusEventHandler(kreis_InvalidRadius);
Die Bindung des Ereignishandlers an das Ereignis erfolgt erst, nachdem der Konstruktoraufruf beendet ist. Folglich kann das Ereignis auch nicht ausgelöst werden, wenn dem parametrisierten Konstruktor der Circle-Klasse ein negativer Radius übergeben wird. Für diese Situation muss eine andere Lösung gefunden werden. Wahrscheinlich ahnen Sie es schon: In diesem Fall bietet sich das Auslösen einer Exception an. Jetzt wollen wir eine eigene Ausnahme bereitstellen, die wir als InvalidMeasureException bezeichnen wollen.
    public class InvalidMeasureException : Exception {
       public InvalidMeasureException() { }
       public InvalidMeasureException (string message) : base(message) { }
       public InvalidMeasureException (string message, Exception inner)
                                      : base(message, inner) { }
    }
In der Klasse sind drei Konstruktoren mit unterschiedlichen Parameterlisten definiert. In der Basisklasse Exception ist der Konstruktor mit derselben Parameterliste überladen. Wir leiten die Parameter deshalb jeweils mit base an den passenden Konstruktor der Basisklasse weiter. Wenn Sie in Tabelle 9.1 schauen, werden Sie feststellen, dass die Klasse Exception 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 InvalidMeasureException("Unzulässiger Radius.", e));
eine neue Exception auslösen und dabei eine passende individuelle Meldung sowie die Referenz auf die ursprüngliche Ausnahme an den Aufrufer übergeben. Die Exception InvalidMeasureException soll natürlich dann ausgelöst werden, wenn die Überprüfung die Unzulässigkeit des Wertes festgestellt hat. Diese Untersuchung geschieht in der Eigenschaftsmethode Radius. Allerdings wollen wir das Ereignis nicht durch die Exception ersetzen, sondern stellen die folgenden Anforderungen an den Code: Die Ausnahme wird in jedem Fall ausgelöst, wenn ein unzulässiger Wert zugewiesen werden soll. Registriert der aufrufende Code einen Ereignishandler für InvalidRadius, muss die Ausnahme nicht behandelt werden. Stattdessen wird die Ausnahme in einer weiteren Eigenschaft des EventArgs-Objekts bereitgestellt. Wird kein Ereignishandler registriert, muss die Ausnahme behandelt werden.
Hinweis
Diese Verhaltensweise, entweder ein Ereignis zu behandeln oder die ausgelöste Ausnahme, findet sich im .NET Framework wieder. Ein gutes Beispiel dafür ist in ADO.NET das Ereignis RowUpdated des DataAdapter-Objekts.
Um den Forderungen zu entsprechen, müssen wir im ersten Schritt die Klasse InvalidRadiusEventArgs überarbeiten. Sie wird um die schreibgeschützte Eigenschaft Error ergänzt. Außerdem erhält der Konstruktor einen zweiten Parameter, der das Exception-Objekt entgegennimmt.
    // geänderte InvalidRadiusEventArgs-Klasse
    public class InvalidRadiusEventArgs : EventArgs {
      private double _Radius;
      private InvalidMeasureException _Error;
      public double Radius {
        get { return _Radius; }
      }
      public InvalidMeasureException Error {
        get { return _Error; }
      }
      public InvalidRadiusEventArgs(double radius, InvalidMeasureException ex){
        _Radius = radius;
        _Error = ex;
      }
    }
Im nächsten Schritt passen wir die Eigenschaft Radius an. Wird die Unzulässigkeit des übergebenen Wertes festgestellt, wird zuerst ein Objekt der Ausnahme InvalidMeasureException erzeugt und anschließend die geschützte Methode OnInvalidRadius aufgerufen, die für die Auslösung des Ereignisses sorgt.
    // überarbeitete Eigenschaft ‘Radius’
    public virtual double Radius {
      get { return _Radius; }
      set {
        if (value >= 0)
          _Radius = value;
        else {
          InvalidMeasureException ex = new InvalidMeasureException
                  ("Ein Radius von " + value + " ist nicht zulässig.");
          OnInvalidateRadius(new InvalidRadiusEventArgs(value, ex));
        }
      }
    }
In der Methode OnInvalidRadius wird noch der else-Zweig ergänzt, in dem die Ausnahme ausgelöst wird, falls kein Ereignishandler registriert ist.
    protected void OnInvalidateRadius(InvalidRadiusEventArgs e){
      if (InvalidRadius != null)
        InvalidRadius(this, e);
      else
        throw e.Error;
    }
Zum Schluss bleibt noch, sich vom Erfolg der Implementierung zu überzeugen. Dazu dient der folgende Beispielcode, in dem beide Varianten einem Test unterzogen werden.
    class Program {
      static void Main(string[] args) {
        Circle kreis1 = null;
        Circle kreis2 = null;
        try {
          kreis1 = new Circle();
          kreis1.InvalidRadius +=
              new InvalidRadiusEventHandler(kreis_InvalidRadius);
          kreis1.Radius = -100;
          kreis2 = new Circle(-89);
          kreis2.Radius = -9;
        }
        catch (InvalidMeasureException ex){
          Console.WriteLine("Im Catch-Block: " + ex.Message);
        }
        Console.ReadLine();
      }
      // der Ereignishandler
      static void kreis_InvalidRadius(object sender, InvalidRadiusEventArgs e) {
        Console.WriteLine("Ereignishandler: " + e.Error.Message);
      }
    }
Anmerkung
In der Gesamtlösung des Beispiels GeometricObjects auf der Buch-DVD unter Beispiele\Kapitel 9\GeometricObjectsSolution sind neben der Änderung an der Klasse Circle auch die entsprechenden Änderungen an der Klasse Rectangle vorgenommen worden.