Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


C# von Eric Gunnerson
Die neue Sprache für Microsofts .NET-Plattform
C# - Zum Katalog
gp Kapitel 5 Ausnahmebehandlung
  gp 5.1 Was ist falsch an Rückgabecodes?
  gp 5.2 try und catch
  gp 5.3 Die Ausnahmehierarchie
  gp 5.4 Übergeben von Ausnahmen an die aufrufende Funktion
  gp 5.5 Benutzerdefinierte Ausnahmeklassen
  gp 5.6 Finally
  gp 5.7 Effizienz und Overhead
  gp 5.8 Entwurfsrichtlinien

Kapitel 5 Ausnahmebehandlung

In vielen Programmierhandbüchern wird der Bereich Ausnahmebehandlung in einem der letzten Kapitel thematisiert. In diesem Buch wird dieses Thema aus verschiedenen Gründen in einem der ersten Kapitel besprochen.

Der erste Grund besteht darin, dass die Ausnahmebehandlung stark in die .NET-Laufzeitumgebung eingebunden ist und daher im C#-Code sehr oft zum Einsatz kommt. C++-Code kann ohne Ausnahmebehandlung geschrieben werden, in C# ist dies nicht möglich.

Der zweite Grund ist der, dass so bessere Codebeispiele verwendet werden können. Wenn die Ausnahmebehandlung erst in einem der letzten Kapitel erläutert wird, kann diese nicht in den Codebeispielen der ersten Kapitel verwendet werden, und das bedeutet, dass die Beispiele nicht mit Hilfe guter Programmiermethoden geschrieben werden können.

Unglücklicherweise heißt das auch, dass Klassen verwendet werden, ohne dass diese richtig eingeführt wurden. Lesen Sie den folgenden Abschnitt, um einen Einblick zu erhalten. Die Klassen werden im nächsten Kapitel ausführlich besprochen.


Galileo Computing

5.1 Was ist falsch an Rückgabecodesdowntop

Die meisten Programmierer haben wahrscheinlich bereits Code geschrieben, der dem folgenden ähnelt:

bool success = CallFunction();
if (!success)
{
    // Fehler verarbeiten
}

Diese Methode funktioniert, es muss jedoch jeder Rückgabewert auf Fehler geprüft werden. Wenn das obige Beispiel als

CallFunction();

geschrieben wäre, würde jede Fehlerrückgabe verworfen werden. Dies ist eine potenzielle Quelle für Bugs.

Es gibt viele verschiedene Modelle für den Kommunikationsstatus; einige Funktionen können einen HRESULT-Wert zurückgeben, andere geben einen booleschen Wert zurück, wieder andere verwenden andere Mechanismen.

In der .NET-Laufzeitumgebung stellen Ausnahmen die fundamentale Methode zur Behandlung von Fehlerbedingungen dar. Ausnahmen sind schöner als Rückgabecodes, da sie nicht einfach ignoriert werden können.


Galileo Computing

5.2 try und catch  downtop

Zur Behandlung von Ausnahmen muss der Code ein wenig anders organisiert werden. Die Codeabschnitte, die zu Ausnahmen führen können, werden in einem try-Block platziert, der Code zur Behandlung von Ausnahmen im try-Block wird in einen catch-Block geschrieben. Beispiel:

using System;
class Test
{
    static int Zero = 0;
    public static void Main()
    {
            // Hier auf Ausnahmen achten
        try
        {
            int j = 22 / Zero;
        }
// Ausnahmen, die im try-Block auftreten, werden hierher 
übermittelt
        catch (Exception e)
        {
            Console.WriteLine("Exception " + e.Message);
        }
        Console.WriteLine("After catch");
    }
}

Der try-Block umfasst einen Ausdruck, der zur Erzeugung einer Ausnahme führt. In diesem Fall wird die Ausnahme DivideByZeroException erzeugt. Wenn die Division stattfindet, stoppt die .NET-Laufzeitumgebung die Codeausführung und sucht nach einem try-Block, der den Code enthält, in dem die Ausnahme auftrat. Sobald ein try-Block ermittelt wurde, wird nach dem entsprechenden catch-Block gesucht.

Ist ein solcher catch-Block vorhanden, wird der geeignetste ausgewählt (zum Auswahlverfahren gleich mehr) und der im catch-Block enthaltene Code wird ausgeführt. Der Code im catch-Block kann das Ereignis verarbeiten oder das Ereignis erneut ausgeben.

Im Beispielcode wird die Ausnahme aufgefangen, anschließend wird die im Ausnahmeobjekt enthaltene Meldung ausgegeben.


Galileo Computing

5.3 Die Ausnahmehierarchie  downtop

Alle C#-Ausnahmen leiten sich aus der Klasse Exception ab, die Teil der Common Language Runtime ist. Wenn eine Ausnahme auftritt, wird der geeignete catch-Block durch einen Abgleich zwischen Typ und Namen der Ausnahme ermittelt. Ein catch-Block mit exakter Übereinstimmung hat hierbei Vorrang vor einer allgemeineren Ausnahme. Zurück zum Beispiel:

using System;
class Test
{
    static int Zero = 0;
    public static void Main()
    {
        try
        {
            int j = 22 / Zero;
        }
            // Spezifische Ausnahme auffangen
        catch (DivideByZeroException e)
        {
            Console.WriteLine("DivideByZero {0}", e);
        }
            // Verbleibende Ausnahmen auffangen
        catch (Exception e)
        {
            Console.WriteLine("Exception {0}", e);
        }
    }
}

Der catch-Block, mit dem die DivideByZeroException aufgefangen wird, stellt die genauere Übereinstimmung dar, daher wird dieser Codeabschnitt ausgeführt.

Das nachfolgende Beispiel ist etwas komplexer:

using System;
class Test
{
    static int Zero = 0;
    static void AFunction()
    {
        int j = 22 / Zero;
            // Die folgende Zeile wird nie ausgeführt.
        Console.WriteLine("In AFunction()");
    }
    public static void Main()
    {
        try
        {
            AFunction();
        }
        catch (DivideByZeroException e)
        {
            Console.WriteLine("DivideByZero {0}", e);
        }
    }
}

Was geschieht hier?

Bei Ausführung der Division wird eine Ausnahme erzeugt. Die Laufzeitumgebung sucht nach einem try-Block in AFunction(), findet jedoch keinen try-Block, daher springt sie aus AFunction() und sucht in Main() nach einem try-Block. Hier ist ein try-Block vorhanden, also wird nach einem entsprechenden catch-Block gesucht. Anschließend wird der catch-Block ausgeführt.

Gelegentlich kann es vorkommen, dass keine der catch-Klauseln passt.

using System;
class Test
{
    static int Zero = 0;
    static void AFunction()
    {
        try
        {
            int j = 22 / Zero;
        }
            // Diese Ausnahme ist nicht geeignet.
        catch (ArgumentOutOfRangeException e)
        {
            Console.WriteLine("OutOfRangeException: 
{0}", e);
        }
        Console.WriteLine("In AFunction()");
    }
    public static void Main()
    {
        try
        {
            AFunction();
        }
            // Diese Ausnahme ist nicht geeignet.
        catch (ArgumentException e)
        {
            Console.WriteLine("ArgumentException {0}", e);
        }
    }
}

Weder der catch-Block in AFunction() noch der catch-Block in Main() entsprechen der ausgegebenen Ausnahme. Wenn dieser Fall eintritt, wird die Ausnahme durch eine Ausnahmebehandlungsroutine aufgefangen, die als letzte Möglichkeit eingesetzt wird. Die von dieser Routine ausgeführte Aktion richtet sich nach der Konfiguration der Laufzeitumgebung, führt üblicherweise jedoch zur Anzeige eines Dialogfeldes mit der Ausnahmeinformation und zum anschließenden Programmstopp.


Galileo Computing

5.4 Übergeben von Ausnahmen an die aufrufende Funktion  downtop

Gelegentlich kann bei Ausgabe einer Ausnahme nicht viel getan werden, da diese durch die aufrufende Funktion verarbeitet werden muss. Es gibt grundsätzlich drei Methoden, die in solchen Fällen angewendet werden können. Die Namen dieser Methoden basieren auf den Ergebnissen, zu denen sie in der aufrufenden Funktion führen: Caller Beware, Caller Confuse und Caller Inform.


Galileo Computing

5.4.1 Caller Beware  downtop

Die erste Methode besteht darin, die Ausnahme nicht aufzufangen. Dies ist manchmal die richtige Entscheidung, kann jedoch dazu führen, dass das Objekt sich nicht im richtigen Status befindet, was wiederum bei einer späteren Verwendung durch die aufrufende Funktion Probleme verursachen kann. Darüber hinaus erhält die aufrufende Funktion möglicherweise nicht genügend Informationen.


Galileo Computing

5.4.2 Caller Confuse  downtop

Die zweite Methode besteht darin, die Ausnahme aufzufangen, eine Bereinigung durchzuführen und die Ausnahme erneut auszugeben:

using System;
public class Summer
{
    int    sum = 0;
    int    count = 0;
    float    average;
    public void DoAverage()
    {
        try
        {
            average = sum / count;
        }
        catch (DivideByZeroException e)
        {
            // Bereinigung hier
            throw e;
        }
    }
}
class Test
{
    public static void Main()
    {
        Summer summer = new Summer();
        try
        {
            summer.DoAverage();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception {0}", e);
        }
    }
}

Diese stellt im Allgemeinen die Ausnahmebehandlung niedrigster Stufe dar; ein Objekt sollte nach einer Ausnahme stets weiterhin über einen gültigen Status verfügen.

Diese Methode wird als Caller Confuse bezeichnet, denn obwohl sich das Objekt nach Auftreten der Ausnahme in einem gültigen Status befindet, besitzt die aufrufende Funktion nur wenige Informationen zum weiteren Vorgehen. In diesem Fall besagt die Ausnahmeinformation, dass irgendwo in der aufrufenden Funktion die Ausnahme DivideByZeroException aufgetreten ist. Es werden jedoch keine detaillierten Informationen zur Ausnahme oder deren Behandlung bereitgestellt. Dies kann in Ordnung sein, wenn die Ausnahme selbsterklärende Informationen zurückgibt.


Galileo Computing

5.4.3 Caller Inform  downtop

Bei der Methode Caller Inform werden zusätzliche Informationen für den Benutzer zurückgegeben. Die aufgefangene Ausnahme wird in einer Ausnahme mit zusätzlichen Informationen platziert.

using System;
public class Summer
{
    int    sum = 0;
    int    count = 0;
    float    average;
    public void DoAverage()
    {
        try
        {
            average = sum / count;
        }
        catch (DivideByZeroException e)
        {
                // Ausnahme in anderer Ausnahme platzieren,
                // zusätzlichen Kontext einfügen.
                throw (new DivideByZeroException(
                    "Count is zero in DoAverage()", e));
        }
    }
}
public class Test
{
    public static void Main()
    {
        Summer summer = new Summer();
        try
        {
            summer.DoAverage();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: {0}", e);
        }
    }
}

Wenn die Ausnahme DivideByZeroException in der DoAverage()-Funktion aufgefangen wird, wird sie in einer neuen Ausnahme platziert, die dem Benutzer zusätzliche Informationen zur Fehlerursache gibt. Üblicherweise ist die umgebende Ausnahme vom gleichen Typ wie die aufgefangene Ausnahme, dies kann jedoch je nach Modell für die aufrufende Funktion variieren.

Dieses Programm erzeugt die folgende Ausgabe:

Exception: System.DivideByZeroException: Count is 
zero in DoAverage() ---> System.DivideByZeroException
   at Summer.DoAverage()
   at Summer.DoAverage()
   at Test.Main()

Im Idealfall platziert jede Funktion, die die Ausnahme erneut ausgeben möchte, die ursprüngliche Ausnahme in einer Ausnahme mit zusätzlichen Kontextinformationen.


Galileo Computing

5.5 Benutzerdefinierte Ausnahmeklassen  downtop

Ein Nachteil des vorangegangenen Beispiels liegt darin, dass die aufrufende Funktion über den Ausnahmetyp nicht ermitteln kann, welche Ausnahme beim Aufruf von DoAverage() aufgetreten ist. Um zu wissen, dass die Ausnahme auftrat, weil der Wert des Zählers Null betrug, hätte die Ausnahmemeldung nach einer entsprechenden Zeichenfolge durchsucht werden müssen (»Count is zero«).

Dies wäre keine gute Idee, da sich der Benutzer nicht darauf verlassen kann, dass der Text auch in späteren Versionen der Klasse unverändert bleibt, und der Autor der Klasse wäre nicht in der Lage, den Text zu ändern. In diesem Fall kann eine neue Ausnahmeklasse erstellt werden.

using System;
public class CountIsZeroException: Exception
{
    public CountIsZeroException()
    {
    }
    public CountIsZeroException(string message)
    : base(message)
    {
    }
    public CountIsZeroException(string message, Exception inner)
    : base(message, inner)
    {
    }
}
public class Summer
{
    int    sum = 0;
    int    count = 0;
    float    average;
    public void DoAverage()
    {
        if (count == 0)
            throw(new CountIsZeroException("Zero
			 count in DoAverage"));
        else
            average = sum / count;
    }
}
class Test
{
    public static void Main()
    {
        Summer summer = new Summer();
        try
        {
            summer.DoAverage();
        }
        catch (CountIsZeroException e)
        {
            Console.WriteLine("CountIsZeroException: {0}", e);
        }
    }
}

DoAverage() untersucht nun, ob eine Ausnahme vorliegt (ob count den Wert 0 aufweist), und erzeugt ggf. die Ausnahme CountIsZeroException.


Galileo Computing

5.6 Finally  downtop

Beim Schreiben einer Funktion muss gelegentlich darauf geachtet werden, dass vor dem Abschließen der Funktion eine Bereinigung durchgeführt werden muss, beispielsweise das Schließen einer Datei. Tritt eine Ausnahme auf, wird diese Bereinigung möglicherweise übersprungen.

using System;
using System.IO;
class Processor
{
    int    count;
    int    sum;
    public int average;
    void CalculateAverage(int countAdd, int sumAdd)
    {
        count += countAdd;
        sum += sumAdd;
        average = sum / count;
    }
    public void ProcessFile()
    {
        FileStream f = new FileStream("data.txt",
		 FileMode.Open);
        try
        {
            StreamReader t = new StreamReader(f);
            string    line;
            while ((line = t.ReadLine()) != null)
            {
                int count;
                int sum;
                count = Int32.FromString(line);
                line = t.ReadLine();
                sum = Int32.FromString(line);
                CalculateAverage(count, sum);
            }
            f.Close();
        }
            // Immer ausführen, bevor Funktion verlassen wird, 
selbst wenn
            // im try-Block eine Ausnahme ausgegeben wurde.
        finally
        {
            f.Close();
        }
    }
}
class Test
{
    public static void Main()
    {
        Processor processor = new Processor();
        try
        {
            processor.ProcessFile();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: {0}", e);
        }
    }
}

In diesem Beispiel wird eine Datei durchlaufen, die count- und sum-Werte werden eingelesen und es wird ein Durchschnittswert berechnet. Was geschieht jedoch, wenn der erste count-Wert der Datei Null beträgt?

In diesem Fall wird für die Division in CalculateAverage() eine DivideByZeroException ausgegeben, und der Lesevorgang für die Datei wird unterbrochen. Wenn der Programmierer die Funktion ohne Berücksichtigung von Ausnahmen geschrieben hat, wird der Aufruf von file.Close() übersprungen und die Datei wird damit nicht geschlossen.

Der Code innerhalb des finally-Blocks wird in jedem Fall vor Verlassen der Funktion ausgeführt, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht. Durch Platzieren des Aufrufs file.Close() im finally-Block wird die Datei in jedem Fall geschlossen.


Galileo Computing

5.7 Effizienz und Overhead  downtop

In Sprachen ohne Speicherbereinigung ist das Hinzufügen von Ausnahmebehandlungsroutinen aufwendig, da alle Objekte innerhalb einer Funktion verfolgt werden müssen, um sicherzustellen, dass diese bei Ausgabe einer Ausnahme ordnungsgemäß zerstört werden. Der erforderliche Ablaufverfolgungscode erhöht sowohl die Ausführungszeit als auch die Codegröße für eine Funktion.

In C# werden Objekte jedoch nicht durch den Compiler, sondern über die Speicherbereinigung verfolgt, sodass die Ausnahmebehandlung leicht zu implementieren ist und nur einen minimalen Laufzeitoverhead erzeugt, wenn kein Ausnahmezustand eintritt.


Galileo Computing

5.8 Entwurfsrichtlinien  toptop

Ausnahmen sollten dazu eingesetzt werden, über Ausnahmebedingungen zu informieren. Verwenden Sie Ausnahmen nicht dazu, über erwartete Ereignisse zu informieren, beispielsweise darüber, dass das Dateiende erreicht wurde. Bei normaler Verwendung einer Klasse sollten keine Ausnahmen ausgegeben werden.

Genauso sollten Sie über Rückgabewerte keine Informationen weiterleiten, die eigentlich in eine Ausnahme gehören.

Befindet sich im System-Namespace eine vordefinierte Ausnahme, die Ihre Ausnahmebedingung beschreibt – eine Ausnahme, die den Benutzern der Klasse verständlich ist – , sollten Sie diese verwenden und nicht statt dessen eine neue Ausnahmeklasse definieren. Neue Ausnahmeklassen können dort eingesetzt werden, wo der Benutzer verschiedene Fehlersituationen mit gleichen Ausnahmen unterscheiden möchte.

Wenn über den Code eine Ausnahme aufgefangen wird, die nicht ohne weiteres behandelt werden kann, sollten Sie in Betracht ziehen, die Ausnahme in einer anderen Ausnahme zu platzieren, bevor die Ausnahme ausgegeben wird.






1    Dies gilt für die .NET-Klassen allgemein, nur in einigen Fällen trifft dies nicht zu.

   

Select * from SQL Server 2000




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de