Exception Handling

Exception Handling ist wichtig und kann in vielen Situationen sehr hilfreich sein. Trotzdem ist das Wissen darüber bei vielen Einsteigern und auch Fortgeschrittenen sehr begrenzt. Viele falsche Annahmen werden über Exceptions gemacht und oft werden diese auch vollkommen falsch angewendet. Dabei sind Exceptions in einigen Situationen mehr als hilfreich, führen zu besserem und übersichtlicherem Code und in anderen Situationen kann man sie schlicht und einfach nicht ignorieren, da man ohne nicht auskommt.

Dieser Artikel ist eine Einführung in Exceptions, der Hintergrundwissen vermittelt und einige Anwendungsfälle von Exception Handling aufzeigt. Er ist an Einsteiger, aber auch Fortgeschrittene gerichtet und soll dem interessierten Leser einfach ein weiteres Werkzeug an die Hand geben, daß ihm die Entwicklung von guten, wartbaren und performanten Code ermöglicht.

Zu Anfang möchte ich versuchen zu erklären, was eine Exception überhaupt ist. Auf Deutsch bedeutet Exception Ausnahme. Synonyme von Ausnahme sind z.B. Abweichung, Anomalie, Regelverstoß und Sonderfall. Dies beschreibt ziemlich gut was eine Exception ist: Es ist eine Abweichung vom Normalen. Es ist also ein Fehler.

Jede Exception in C# bzw. .NET wird von der Basisklasse System.Exception abgeleitet. Diese Information ist wichtig für den später beschrieben Fall der Fehlerbehandlung, sowie für das Erzeugen von eigenen Exceptions.

Noch schnell ein paar Überlegungen zur Leistung von Exceptions, bevor es ans Eingemachte geht. Das Überwachen von Ausnahmen kostet praktisch keine Rechenzeit, solange keine Exception geworfen wird. Es ist also praktisch kostenlos und daher in Optimierungs- und Performance-Fragen zu vernachlässigen. Das Auslösen und Behandeln kostet jedoch einen deutlichen Anteil von Systemressourcen und auch Ausführungszeit. Dies ist nicht mehr zu vernachlässigen. Laut MSDN und auch laut gängiger Erfahrung, sollten Ausnahmen also nur zur Behandlung von außergewöhnlichen Bedingungen eingesetzt werden. Die Behandlung von vorhersagbaren Ereignissen oder die Verwendung zur Ablaufsteuerung hingegen ist sehr schlechter Programmierstil und aus Performance-Sicht eher problematisch als hilfreich.

Was sind außergewöhnliche Bedingungen?

Fehlende (ArgumentNullException) oder ungültige (InvalidParameterException) sind Beispiele für außergewöhnliche Bedingungen. Eine Methode kann erwarten, daß sie mit korrekten und anständigen Parametern aufgerufen wird. Ist dies nicht so, ist ein probates Mittel das Werfen einer entsprechenden Exception. Im fertigen Programm wird diese kaum negative Performance-Charakteristiken aufweisen, während der Entwicklung ist dies jedoch ein sehr gutes und einfaches Mittel den Entwickler auf Probleme hinzuweisen.

Weitere gute Beispiele sind natürlich Zugriffe auf Ressourcen, die auch von anderen Stellen des Systems beeinflusst werden können. Dazu gehören Dateien im Filesystem, aber auch TCP-Sockets und sogar der Arbeitsspeicher. Wie wir im folgenden Verlauf des Artikels feststellen werden, reicht es nämlich oft nicht aus, zu prüfen, ob eine Datei vorhanden ist und diese Tatsache im Misserfolg entsprechend zu behandeln. Zwischen der Prüfung und dem öffnen der Datei kann diese nämlich immer noch verschwinden.

Die Syntax

Die Syntax von Exceptions ist einfach. Hier ein kleines Beispiel, daß ich der MSDN entnommen habe:

object obj = null;
try
{
  int i = (int)o2; // object kann nicht in int gecastet werden -> Exception
}
catch (InvalidCastException e)
{
  // Fehlerbehandlung
}

Dieses Beispiel macht in dieser Form nicht viel Sinn, da es schlechter Programmierstil ist, dazu später aber noch mehr. Trotzdem ist es gut geeignet aufzuzeigen wie Exception-Handling funktioniert. Wir schließen die zu überwachenden Programmzeilen ganz einfach in einem try-Block ein. Try bedeutet Versuch, es wird also versucht, diese Anweisungen auszuführen. Schlägt dies fehl, also wirft eine dieser Anweisungen eine Exception, so springt das Programm in den Catch-Block. In diesem kann nun die Ausnahme behandelt werden, also z.B. kann dem Benutzer eine Meldung ausgegeben werden. Der Catch-Block wird komplett ignoriert, wenn keine Exception auftritt. Das bedeutet, daß die Anweisungen in diesem Block nicht ausgeführt werden.

Selbstverständlich können auch mehrere, unterschiedliche Ausnahmen auf unterschiedliche Art und Weise behandelt werden. Die Syntax dafür ist auch nicht viel schwerer:

object obj = null;
try
{
  int i = (int)o2; // object kann nicht in int gecastet werden -> Exception
}
catch (InvalidCastException e)
{
  // Fehlerbehandlung bei ungültigem Cast
}
catch (Exception e)
{
  // alle anderen Ausnahmen
}

Dieses Beispiel zeigt, daß wir einfach mehrere catch-Blöcke verwenden können um unterschiedliche Ausnahmen zu behandeln. Die Anzahl der catch-Blöcke ist dabei nicht limitiert.

Das vorhergehende Beispiel zeigt noch eine kleine Besonderheit auf. In dem wir die Basis-Exception Exception fangen (wir erinnern uns, alle Ausnahmen werden von der Basisklasse System.Exception abgeleitet) behandeln wir automatisch alle Ausnahmen, die nicht InvalidCastException sind. Das ist der klassische Fall eines unerwarteten Fehlers über den ja bekanntlich ziemlich häufig Witze gemacht werden.

Man könnte nun meinen, daß es doch sinnvoll wäre, einfach alle Exceptions mit einem catch (Exception e) zu fangen und so wunschlos glücklich zu sein. Nein, leider ist es nicht ganz so einfach. Es würden in diesem Fall zwar alle Ausnahmen gefangen, aber man kann diese nicht mehr unterscheiden. Dies kann in bestimmten Situationen Sinn machen, aber nicht immer. Wir sollten, wenn wir eine Exception identifizieren und gesondert behandeln können, immer den spezialisierten Catch-Block verwenden.

Aufräumen

Das war aber noch nicht alles. Manchmal müssen Anweisungen ausgeführt werden, unabhängig davon, ob eine Exception auftritt oder nicht. Dies ist eigentlich immer dann der Fall, wenn wir aufräumen möchten. Zu diesem Zweck gibt es den Finally-Block. Dieser kann z.B. wie folgt angewendet werden (Pseudocode):

public void SaveHighscore()
{
  try
  {
    FileStream highscoreFile = OpenFile("highscore.dat");
    highscoreFile.MoveToEnd();
    highscoreFile.Write(score);
  }
  catch (FileNotFoundException e)
  {
    CreateNewHighscoreFile();
  }
  catch (ReadError e)
  {
    DisplayErrorMessage("Konnte Datei nicht lesen");
  }
  finally
  {
    highscoreFile.Close();
    highscoreFile.Dispose();
  }
}

Wir schreiben also eine Punktzahl in ein imaginäres Highscore-File. Dabei können zwei Fehler auftreten, zum einen eine FileNotFound-Ausnahme. In diesem Fall erzeugen wir einfach ein leeres Highscore-File. Und zum anderen ein Lesefehler. In diesem Fall zeigen wir dem Spieler einfach eine Fehlermeldung an.

Interessant ist nun der Finally-Block. Dieser wird unabhängig davon ausgeführt, ob eine Exception gefangen wurde oder nicht. Dies klingt erst einmal nicht sonderlich interessant und erst Recht nicht wichtig. Ist es aber. Da ich bereits einen eigenen Artikel zu den Besonderheiten und zum Sinn von Finally geschrieben habe, möchte ich an dieser Stelle auf diesen verweisen, da das Thema dort umfassend behandelt wird. Dieser Artikel heißt …Finally…

Arten der Fehlerbehandlung

Eric Lippert hat es 2008 in seinem Blog auf den Punkt gebracht und da ich seine Erklärungen ziemlich gut finde, werde ich diese als Grundlage für den folgenden Abschnitt verwenden. Er unterscheidet zwischen vier Arten von Fehlerbehandlung:

  • Kritische („fatal“)
  • Holzkopf („boneheaded“)
  • Irritierende („vexing“)
  • Äußerlich entstehende („exogenous“)

Kritische Ausnahmen

Kritische Fehler können nicht vom Entwickler verhindert werden, er ist nicht daran schuld und vor allem kann er sie nicht auflösen. Dies sind Dinge wie „Kein Speicher“ und „Thread abgebrochen“. Diese treten irgendwo in den Tiefen des Systems auf und man kann in der Regel nichts dagegen tun. Catch-Blöcke sind oft nicht sonderlich hilfreich. Oft macht es Sinn, an dieser Stelle einfach dem Benutzer eine anständige Fehlermeldung anzuzeigen und das Programm zu beenden. Der viel zitierte BlueScreen von Windows ist ein gutes Beispiel.

Holzkopf Ausnahmen

Holzkopf-Ausnahmen sind dein Fehler. Du hast sie verursacht und bist dafür zuständig, diese zu verhindern, denn es sind schlicht und einfach Programmfehler. Diese solltest du selbstverständlich nicht abfangen, sondern du solltest einfach die Fehler in deinem Programm beheben, damit die Exceptions gar nicht erst auftreten. Übergebene Null-Parameter (ArgumentNullException), ungültige Umwandlungen (InvalidCastException), Zugriffe außerhalb von Array-Grenzen (IndexOutOfRangeException) und Teilen durch Null (DivideByZeroException) sind Beispiele für Holzkopf-Ausnahmen. All diese Fehler können vermieden werden, bevor sie auftreten. Nur Holzköpfe würden versuchen, die Auswirkungen eines Fehlers zu beheben, anstatt den Fehler selbst, daher auch der Name.

Irritierende Ausnahmen

Diese Ausnahmen sind Exceptions, die oft durch schlechte Design-Entscheidungen entstanden sind. Diese Ausnahmen werden an Stellen geworfen, wo sie keinen Sinn machen und eher hinderlich sind. Leider müssen sie deshalb immer behandelt werden.

Das klassische Beispiel für eine Irritierende Ausnahme ist Int32.Parse. Diese Methode wirft eine Exception, wenn der übergebene String nicht als Integer übersetzt werden kann. Da jedoch in 99% der Fälle der Benutzer eine Eingabe macht, die natürlich falsch sein kann, ist es nichts besonderes, daß der String nicht übersetzt werden kann. Es ist einfach keine Ausnahme. Der Entwickler hat auch keine Möglichkeit vorher zu prüfen, ob eine Ausnahme auftreten wird, da er dafür die gesamte Parse-Methode selbst implementieren müsste.

Glücklicherweise hat das .NET-Framework-Team diese schlechte Designentscheidung später durch die Methode TryParse korrigiert.

Äußerlich entstehende Ausnahmen

Die äußerlich entstehenden Ausnahmen sehen auf den ersten Blick so aus wie die irritierenden Ausnahmen, sie sind jedoch nicht das Resultat einer schlechten Design-Entscheidung. Es sind ganz einfach externe Abhängigkeiten, die zu Fehlern führen können. Hier ein Beispiel in Pseudo-C#-Code:

try
{
  using ( File f = OpenFile(filename, ForReading) )
  {
    // Blah blah blah
  }
}
catch (FileNotFoundException)
{
  // Handle filename not found
}

Viel zu kompliziert? Man könnte das doch auch so schreiben:

if (!FileExists(filename))
{
  // Handle filename not found
}
else
{
  using ( File f = OpenFile(filename, ForReading) )
  {
    // Blah blah blah
  }
}

Viel einfacher und vor allem ohne Exceptions. Ja, aber es gibt einen feinen Unterschied. Der zweite Code verursacht in gewissen Fällen eine sogenannte Race Condition. Zwischen der Prüfung ob die Datei existiert (Zeile 01) und dem tatsächlichen Öffnen der Datei (Zeile 07) kann nicht garantiert werden, daß ein anderer Prozess die Datei verschwinden lässt oder sperrt.

Auch wenn wir die Datei vorher für den exklusiven Zugriff durch uns sperren, dann ist das nicht sicher. Jemand könnte das Speichermedium entfernt haben, das Netzwerkkabel gezogen haben, etc.

Äußerlich entstehende Ausnahmen müssen immer abgefangen werden. Sie können nicht verhindert werden, egal wie sehr man es versucht; Sie sind außerhalb deiner Kontrolle.

Zusammenfassung

  • Versuche kritische Fehler nicht abzufangen. Du kannst nichts dagegen machen und der Versuch, die Ursachen zu beheben, macht es oft nur noch schlimmer.
  • Korrigiere Fehler in deinem Code. Holzkopf-Ausnahmen dürfen in produktivem Code nicht auftreten.
  • Verwende alternative Methoden um Irritierende Ausnahmen zu vermeiden und um diese nicht abfangen zu müssen.
  • Behandele äußerlich entstehende Ausnahmen in jedem Fall. Diese können jederzeit auftreten.

Abschließende Worte

In diesem Artikel habe ich versucht zu erklären, was Exceptions sind und wann und wie sie eingesetzt werden sollten. Ich hoffe, daß der Einsteiger und auch der Fortgeschrittene einige interessante Gesichtspunkte finden konnte und so sein Wissen ausbauen konnte. Die Ausnahmebehandlung sieht auf den ersten Blick sehr einfach aus aber in diesem Artikel sollte klar geworden sein, daß es den ein oder anderen Fallstrick gibt und einige Aspekte, die sich im Schatten verbergen und die man gerne mal übersieht.

Eine Diskussion, Fragen oder Anregungen sind in den Kommentaren dieses Beitrag (und natürlich auch aller anderen) gerne gesehen.

Advertisements

Veröffentlicht am 02.02.2011 in C#, Grundlagen und mit , , , , , , getaggt. Setze ein Lesezeichen auf den Permalink. 6 Kommentare.

  1. Sehr informativ dieser Artikel!
    Das nächste mal, wenn einer im Forum das ganze falsch verwendet,
    schicke ich ihm den Link zu diesem Artikel 🙂

    Weiter so. Freue mich auf weitere Artikel von dir.

  2. Guter Artikel!

    Es heißt allerdings DIE Syntax.

  3. Sehr interessanter Artikel.
    Der äußerliche Fehler ist ziemlich interessant, denn ich habe es bis jetzt meistens so gelöst wie es nicht sein sollte. Den Begriff race conditions habe ich bis jetzt zum ersten mal gehört. In Zukunft werde ich sicherlich etwas anders programmieren ;).
    Danke.

    • Freut mich natürlich, daß ich dir helfen konnte 🙂

      Ja, diese Race-Conditions sind übel und führen zu Fehlern, die man später praktisch nicht findet bzw. nur mit extremen Debugging-Aufwand. Oft werden genau solche Fehler dann als Fehler abgestempelt, die nicht reproduzierbar sind.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: