Programmieren in C#: Einführung


Kapitel 9: Ausnahmen


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


9.1 Allgemeines

Funktionen im Fehlerfall abbrechen

C# verwendet wie viele andere objektorientierte Programmiersprachen Ausnahmen, um Methoden im Fehlerfall abzubrechen und den Aufrufer über den Fehlerfall zu informieren. Dabei werden in C# zwei Schlüsselwörter eingesetzt, die so auch in anderen Programmiersprachen zur Behandlung von Ausnahmen verwendet werden: try und catch. Wie das Arbeiten mit Ausnahmen in C# im Detail aussieht, lernen Sie in diesem Kapitel kennen.


9.2 Ausnahmebehandlung

Ausnahmen fangen und bearbeiten

Wenn während der Ausführung einer Methode festgestellt wird, dass die Methode die Aufgabe, für die sie definiert wurde, nicht erfolgreich ausführen kann, muss sie dies in irgendeiner Weise dem Aufrufer mitteilen. In C# geschieht dies wie in vielen anderen objektorientierten Programmiersprachen auch über Ausnahmen.

Eine Ausnahmesituation tritt ein, wenn eine Methode nicht das tun kann, was sie tun soll. Die Ausführung der Methode wird in diesem Fall abgebrochen. Dies geschieht, indem eine Ausnahme geworfen wird. Da es viele Gründe geben kann, warum eine Methode abgebrochen wird, müssen Ausnahmesituationen identifiziert werden können. Das ist notwendig, damit der Aufrufer auf unterschiedliche Ausnahmesituationen geeignet reagieren kann.

Ausnahmesituationen werden über Klassen identifiziert, die alle von der Klasse Exception im Namensraum System abgeleitet sind. Wenn man davon spricht, dass eine Ausnahme geworfen wird, bedeutet dies, dass die entsprechende Klasse, die die Ausnahmesituation identifiziert, instantiiert wird und das entsprechende Objekt geworfen wird. Dabei wird die Ausführung der Methode abgebrochen und das Objekt eine Ebene nach oben zum Aufrufer geworfen. Indem der Aufrufer überprüft, ob ein entsprechendes Objekt geworfen wird, das eine Ausnahme kennzeichnet, kann er auf den Fehler reagieren.

Der Favoriten-Manager, der in diesem Buch entwickelt wurde, macht es einfach, Favoriten zu verwalten. In gewisser Weise verhält er sich wie ein Array, das eine bestimmte Anzahl Favoriten speichert. Um den Zugriff auf Favoriten zu vereinfachen, soll im Folgenden die Klasse FavoriteManager um einen Indexer erweitert werden. Es handelt sich dabei um eine Eigenschaft, die es ermöglicht, den Favoriten-Manager wie ein Array zu verwenden und über einen Index in eckigen Klammern auf Favoriten zuzugreifen.

using System; 
using System.Collections.Generic; 

public class FavoriteManager 
{ 
  List<Favorite> favs = new List<Favorite>(); 

  public int Length 
  { 
    get 
    { 
      return favs.Count; 
    } 
  } 

  public Favorite this[int i] 
  { 
    set 
    { 
      favs[i] = value; 
    } 

    get 
    { 
      return favs[i]; 
    } 
  } 
} 

Ein Indexer wird definiert, indem einer Klasse eine Eigenschaft hinzugefügt wird, die this heißt. Hinter diesem Schlüsselwort muss außerdem in eckigen Klammern eine Parameterliste angegeben werden. Beachten Sie, dass zur Definition eines Indexers eckige Klammern und nicht runde verwendet werden.

Innerhalb von set und get kann wie bei Eigenschaften gewohnt beliebiger Code ausgeführt werden. So greift der Indexer im obigen Beispielcode auf die Liste favs zu, um über den entsprechenden Index einen Favoriten zurückzugeben oder zu setzen.

Während der Indexer korrekt implementiert ist, besteht die Gefahr, dass bei einem Zugriff ein ungültiger Index übergeben wird. Wenn im Favoriten-Manager zum Beispiel nur drei Favoriten gespeichert sind, bei einem Zugriff aber ein Index von 10 angegeben wird, ist klar, dass auf ein Element in der Liste zugegriffen werden soll, das es gar nicht gibt.

Der Zugriff auf Arrays oder ähnliche Auflistungen mit einem ungültigen Index ist der Paradefall für Ausnahmen. Es ist nicht möglich, im Vorhinein zu verhindern, dass zur Laufzeit ein ungültiger Index angegeben wird. Passiert dies, tritt eine Ausnahmesituation ein. Denn welchen Favoriten soll favs zurückgeben, wenn der Index größer ist als die Anzahl der Favoriten, die in der Liste gespeichert sind?

Wenn mit einem ungültigen Index auf ein Array oder eine andere Auflistung zugegriffen wird, wird typischerweise eine Ausnahme vom Typ ArgumentOutOfRangeException geworfen. Diese Klasse ist im Namensraum System definiert und ist über mehrere Generationen von der Klasse Exception abgeleitet - der Klasse, von der alle Klassen abgeleitet sein müssen, die zur Identifizierung von Ausnahmesituationen verwendet werden.

Woher wissen Sie, dass bei einem Zugriff mit einem ungültigem Index auf ein Array ArgumentOutOfRangeException geworfen wird? Hier hilft nur der Blick in die Dokumentation. So ist in der Dokumentation des Indexers der Klasse Array angegeben, dass eine Ausnahme vom Typ ArgumentOutOfRangeException geworfen wird, wenn der Index kleiner als 0 oder größer oder gleich der Anzahl der Elemente im Array ist. Die gleiche Information finden Sie auch in der Dokumentation des Indexers der Klasse List.

Um auf eine Ausnahme zu reagieren und sie zu behandeln, muss eine try-catch-Klausel verwendet werden. Dazu soll im Folgenden beim Start des Browsers über den Indexer auf den Favoriten mit dem Index 10 im Favoriten-Manager zugegriffen und der Name in einem Dialogfenster ausgegeben werden.

using System; 
using System.Windows.Forms; 

namespace Browser 
{ 
  public partial class Form1 : Form 
  { 
    public Form1() 
    { 
      InitializeComponent(); 
      MessageBox.Show(favoriteManager1[10].Name); 
    } 
  } 
} 

Wenn Sie das Programm ausführen und im Favoriten-Manager nicht zehn Favoriten voreingestellt haben, stürzt es ab. Es erscheint eine Fehlermeldung, die den Anwender darauf hinweist, dass das Programm aufgrund eines Problems beendet werden muss.

Eine Ausnahme wird, wenn sie in einer Methode geworfen wird, an den Aufrufer der Methode übergegeben. Wird keine passende try-catch-Klausel im Aufrufer gefunden, wird die Ausnahme an dessen Aufrufer übergeben. Auf diese Weise wandert die Ausnahme durch alle Methodenaufrufe, bis sie in der Methode ankommt, mit der ein Programm started: Main(). Wird auch in dieser Methode die Ausnahme nicht abgefangen, ist die Standardreaktion von .NET-Programmen die, dass sie zwangsweise beendet werden. Das ist nur folgerichtig, denn der Grund für eine Ausnahme ist, dass eine Methode fehlschlägt und nicht weiß, wie sie die gewünschte Funktion ausführen soll. Wenn Sie es auch nicht wissen und nicht über eine geeignete try-catch-Klausel auf die Ausnahme reagieren, wird das Programm zwangsweise beendet.

Im Folgenden soll nun dem Konstruktor von Form1 eine try-catch-Klausel hinzugefügt werden.

using System; 
using System.Windows.Forms; 

namespace Browser 
{ 
  public partial class Form1 : Form 
  { 
    public Form1() 
    { 
      try 
      { 
        InitializeComponent(); 
        MessageBox.Show(favoriteManager1[10].Name); 
      } 
      catch (ArgumentOutOfRangeException) 
      { 
      } 
    } 
  } 
} 

Die try-catch-Klausel besteht aus zwei Anweisungsblöcken, denen die Schlüsselwörter try und catch voranstehen. Im try-Anweisungsblock wird der Code angegeben, der ausgeführt und daraufhin überprüft werden soll, ob eine Ausnahmesituation eintritt. Wird eine Ausnahme geworfen, soll diese im catch-Anweisungsblock gefangen werden, um auf sie zu reagieren.

Beachten Sie, dass einem try-Anweisungsblock beliebig viele catch-Anweisungsblöcke folgen können. Mehrere catch-Anweisungsblöcke können deswegen notwendig sein, da der Code im try-Anweisungsblock unterschiedliche Ausnahmen werfen kann.

Mit jedem catch-Anweisungsblock kann auf genau eine Ausnahmesituation reagiert werden. Dazu wird der Datentyp, der die entsprechende Ausnahmesituation identifiziert, in Klammern hinter catch angegeben. Im obigen Beispielcode werden demnach vom catch-Anweisungsblock Ausnahmen vom Typ ArgumentOutOfRangeException abgefangen.

Die Zuordnung von einem catch-Anweisungsblock zu einer Ausnahme erfolgt über den Datentyp, der in Klammern hinter catch angegeben ist. Sie können sich diese Klammer wie eine Parameterliste einer Methode vorstellen. So ist es auch möglich, hinter dem Datentypen einen Variablennamen anzugeben, über den auf das entsprechende Objekt zugegriffen werden kann, das geworfen wurde.

using System; 
using System.Windows.Forms; 

namespace Browser 
{ 
  public partial class Form1 : Form 
  { 
    public Form1() 
    { 
      try 
      { 
        InitializeComponent(); 
        MessageBox.Show(favoriteManager1[10].Name); 
      } 
      catch (ArgumentOutOfRangeException ex) 
      { 
        MessageBox.Show(ex.Message); 
      } 
    } 
  } 
} 

In der Klasse Exception sind verschiedene Eigenschaften definiert, die aufgrund der Vererbung auch von einem Objekt vom Typ ArgumentOutOfRangeException angeboten werden. Eine dieser Eigenschaften ist Message. Diese Eigenschaft gibt eine Fehlermeldung vom Typ string zurück, die im obigen Beispielcode in einem Dialogfenster ausgegeben wird.

Eine nützliche Eigenschaft, die in Exception definiert ist, ist StackTrace. Der String, der von dieser Eigenschaft zurückgegeben wird, zeigt an, welche Methode eine Ausnahme geworfen hat. Das ist vor allem dann wichtig, wenn eine Ausnahme von mehreren verschachtelten Methodenaufrufen weitergereicht wird, bis sie in einem catch-Block abgefangen wird. Um herauszufinden, welche Methode für die Ausnahme verantwortlich ist, kann der sogenannte Stack-Trace hilfreich sein.

using System; 
using System.Windows.Forms; 

namespace Browser 
{ 
  static class Program 
  { 
    [STAThread] 
    static void Main() 
    { 
      try 
      { 
        Application.EnableVisualStyles(); 
        Application.SetCompatibleTextRenderingDefault(false); 
        Application.Run(new Form1()); 
      } 
      catch (ArgumentOutOfRangeException ex) 
      { 
        MessageBox.Show(ex.StackTrace); 
      } 
    } 
  } 
} 

Die try-catch-Klausel wird nun nicht mehr im Konstruktor von Form1 verwendet, sondern innerhalb der Methode Main(), mit der der Browser startet. Wenn nun im Konstruktor von Form1 mit einem ungültigen Index auf einen Favoriten im Favoriten-Manager zugegriffen wird, wird eine Ausnahme vom Typ ArgumentOutOfRangeException geworfen. Dies geschieht genaugenommen im Indexer von List.

Das entsprechende Objekt vom Typ ArgumentOutOfRangeException wird vom Indexer von List an den Indexer von FavoriteManager und von dort an den Konstruktor von Form1 übergeben. Wenn sich auch dort kein catch-Anweisungsblock befindet, mit dem diese Ausnahme abgefangen werden kann, wird das Objekt nochmal eine Ebene nach oben geworfen. Da sich in der Methode Main() ein entsprechender catch-Anweisungsblock befindet, wird hier auf die Ausnahmesituation reagiert. Um nun herauszufinden, wo denn genau im Programm die entsprechende Ausnahme geworfen wurde, kann auf die Eigenschaft StackTrace zugegriffen werden.


9.3 Ausnahmen werfen

Ausnahmen explizit unterstützen

In der Softwareentwicklung mit C# werden Sie recht häufig Ausnahmen fangen und behandeln müssen. Viele Methoden im .NET-Framework werfen im Fehlerfall eine Ausnahme, auf die Sie reagieren können. Sie müssen Ausnahmen nicht abfangen. Wenn dann jedoch eine derartige Ausnahme geworfen wird, stürzt Ihr Programm ab. Es ist daher meist von Vorteil, Ausnahmen in irgendeiner Weise zu behandeln, um zu verhindern, dass Ihr Programm vor den Augen eines Anwenders mit einer kryptischen Fehlermeldung beendet wird.

Neben dem Abfangen von Ausnahmen können Sie in Ihren Methoden auch Ausnahmen selbst werfen. Dies geschieht mit dem Schlüsselwort throw. So wird im Folgenden der Indexer des Favoriten-Managers prüfen, ob der übergebene Index ungültig ist und in diesem Fall selbst eine Ausnahme werfen.

using System; 
using System.Collections.Generic; 

public class FavoriteManager 
{ 
  List<Favorite> favs = new List<Favorite>(); 

  public int Length 
  { 
    get 
    { 
      return favs.Count; 
    } 
  } 

  public Favorite this[int i] 
  { 
    set 
    { 
      if (i < 0 || i >= favs.Count) 
        throw new ArgumentOutOfRangeException("i", i, "Ungültiger Index"); 
      favs[i] = value; 
    } 

    get 
    { 
      if (i < 0 || i >= favs.Count) 
        throw new ArgumentOutOfRangeException("i", i, "Ungültiger Index"); 
      return favs[i]; 
    } 
  } 
} 

Der Indexer des Favoriten-Manager überprüft, ob der Parameter i kleiner als 0 oder gleich oder größer als die Anzahl der Elemente in der Liste favs ist. Ist der Index ungültig, wird die Klasse ArgumentOutOfRangeException instantiiert, um dem Aufrufer mitzuteilen, dass ein Parameter ungültig ist. Diese Klasse bietet unter anderem einen Konstruktor an, um ein Objekt mit dem Namen des Parameters, seinem Wert und einer Fehlerbeschreibung zu initialisieren. Diese Daten sollen dem Aufrufer helfen, den Grund der Ausnahme zu verstehen, um gegebenfalls den Fehler im Programm zu beheben.

Um die Ausnahme vom Typ ArgumentOutOfRangeException zu werfen, muss das entsprechende Objekt, das wie gewohnt mit new erstellt wird, hinter throw angegeben werden. Trifft die Codeausführung auf ein throw, wird das entsprechende Objekt sofort eine Ebene nach oben an den Aufrufer übergeben. Nach einem throw wird demnach kein anderer Code mehr in der Methode ausgeführt, die die Ausnahme wirft.

Es gibt einige Klassen, die recht häufig verwendet werden, um Ausnahmen zu werfen:

  • ArgumentOutOfRangeException wird wie gesehen verwendet, wenn der Wert eines numerischen Parameters außerhalb einer gültigen Bandbreite liegt.

  • ArgumentException wird eingesetzt, wenn ein Parameter ungültig ist. Es handelt sich hierbei um eine allgemeinere Klasse als ArgumentOutOfRangeException, die für Parameter verwendet werden kann, die nicht numerisch sind.

  • ArgumentNullException wird verwendet, wenn ein Parameter auf null gesetzt ist, obwohl er nicht auf null gesetzt sein darf. Auch diese Klasse ist von ArgumentException abgeleitet.

  • InvalidOperationException wird verwendet, wenn eine Methode für ein Objekt nicht ausgeführt werden darf. Grund könnte sein, dass die entsprechende Methode im Moment noch nicht vollständig definiert ist oder das sich ein Objekt in einem Zustand befindet, in dem der Aufruf einer Methode keinen Sinn macht.

Die obigen Klassen sind alle im Namensraum System definiert. In den anderen Namensräumen des .NET-Frameworks finden sich aber viele weitere Klassen, die für Ausnahmen verwendet werden können. Es gibt also nicht einen Namensraum, in dem sich alle Ausnahmeklassen befinden. Sie befinden sich mit herkömlichen Klassen und Interfaces jeweils in den Namensräumen, in die sie thematisch passen.


9.4 Ausnahmeklassen entwickeln

Klassen zur Identifikation neuer Ausnahmesituationen

Wenn Sie in Ihrem Code eine Ausnahme werfen möchten und auf der Suche nach der passenden Klasse nicht fündig werden, können Sie eine neue Klasse entwickeln. Sie müssen sie lediglich von Exception oder einer anderen Ausnahmeklasse ableiten. Außerdem sollten Sie vier Konstruktoren definieren.

using System; 
using System.Runtime.Serialization; 

[Serializable] 
public class FavoriteManagerException : Exception 
{ 
  public FavoriteManagerException() 
  { 
  } 

  public FavoriteManagerException(string message) 
    : base(message) 
  { 
  } 

  public FavoriteManagerException(string message, Exception innerException) 
    : base(message, innerException) 
  { 
  } 

  protected FavoriteManagerException(SerializationInfo info, StreamingContext context) 
    : base(info, context) 
  { 
  } 
} 

Neben dem Standardkonstruktor sollten Ihre eigenen Ausnahmeklassen einen Konstruktor anbieten, dem eine Fehlerbeschreibung vom Typ string übergeben werden kann. Außerdem sollte ein Konstruktor zur Verfügung gestellt werden, um die Fehlerbeschreibung und zusätzlich ein bereits existierendes Ausnahme-Objekt zu übergeben. Dieser Konstruktor wird benötigt, wenn in einem catch-Anweisungsblock eine Ausnahme abgefangen wird, dann jedoch im gleichen Anweisungsblock eine neue Ausnahme geworfen werden soll und dieser neuen Ausnahme das bereits existierende Ausnahme-Objekt huckepack mitgegeben werden soll.

Der vierte Konstruktor, der zwei Parameter vom Typ SerializationInfo und StreamingContext erwartet, wird verwendet, um Ausnahme-Objekte serialisieren zu können. Dabei wird ein Objekt in eine Abfolge von Bytes umgewandelt, die zum Beispiel über eine Netzwerkverbindung an einen anderen Computer übertragen werden können, um das Objekt dort wieder zum Leben zu erwecken. Was sich hinter der Serialisierung genau verbirgt, spielt an dieser Stelle keine Rolle. Weil Microsoft in den Richtlinien zum Erstellen von Ausnahmeklassen empfiehlt, Ausnahmeklassen serialisierbar zu machen, muss dieser vierte Konstruktor definiert werden. Außerdem muss in eckigen Klammern vor dem Klassennamen ein Attribut namens Serializable gesetzt werden. Auch dieses können Sie an dieser Stelle ignorieren.


9.5 Zusammenfassung

Fit für den Praxiseinsatz

Ausnahmen sind ein kleines und einfaches Thema, so dass Sie nach der Lektüre dieses Kapitels Ausnahmen problemlos in Ihren Anwendungen fangen und behandeln können. Die Dokumentation zu Ausnahmen und zur Ausnahmebehandlung im C#-Programmierhandbuch enthält ein paar weitere Details. So wird dort zum Beispiel ein Schlüsselwort finally vorgestellt, mit dem ein zusätzlicher Anweisungsblock hinter einer try-catch-Klausel angegeben werden kann. Da finally optional ist und häufig nicht benötigt wird, können Sie sich diese Details später ansehen, wenn Sie die Grundlagen der C#-Programmierung, die Sie in diesem Buch gelernt haben, verinnerlicht haben und bereit für die nächsten Schritte sind.


9.6 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Im Falle eines fehlerhaften Castings wird eine Ausnahme vom Typ InvalidCastException geworfen. Diese Ausnahme ist im Namensraum System definiert. Ergänzen Sie Ihre Lösung zur Aufgabe 3 aus Abschnitt 8.9, „Aufgaben“ um entsprechende try-catch-Klauseln überall da, wo Sie Datentypen casten. Geben Sie im catch-Anweisungsblock das Ausnahme-Objekt per Trace aus. Fügen Sie zum Test Ihrer Lösung eine Zeile hinzu, in der Sie absichtlich zu einem ungültigen Datentyp casten.

    Suchen Sie im Favoriten-Manager nach den Zeilen, in denen Sie Datentypen casten, und fügen Sie den entsprechenden Methoden wie in diesem Kapitel gesehen try-catch-Klauseln hinzu.

  2. Stellen Sie sicher, dass die Konstruktoren der Klasse Favorite in Ihrer Lösung zur Aufgabe 1 keine Parameter akzeptieren, die null sind. Werfen Sie in diesem Fall eine Ausnahme vom Typ ArgumentNullException. Erstellen Sie zum Test ein Objekt vom Typ Favorite und übergeben Sie null-Werte als Parameter an den Konstruktor.

    Überprüfen Sie mit Hilfe einer if-Kontrollstruktur, ob die Parameter im Konstruktor der Klasse Favorite gleich null sind, und werfen Sie in diesem Fall eine Ausnahme mit throw.