ein Projekt von goloroden.de

Ausnahmen

Was sind Ausnahmen?

Wird die Frage gestellt, aus welchem Grund Anwendungen entwickelt werden, so geschieht dies zunächst im Wesentlichen zur Bewältigung einer Aufgabe und zur Lösung der mit dieser Aufgabe einhergehenden Problemen. Obwohl dies den initialen Beweggrund darstellt, enthält jede Anwendung zahlreiche weitere Aspekte, die bei der Entwicklung neben der eigentlichen Domäne beachtet werden müssen.

Dazu zählen beispielsweise Aspekte wie Sicherheit, Ausführungsgeschwindigkeit oder Stabilität. Ein wesentlicher Faktor, der sich in direkter Konsequenz auf die Qualität einer jeden Anwendung auswirkt, ist der Umgang mit potenziellen Fehlern, die während der Ausführung der Anwendung auftreten können.

Anwendungen, die auf Basis der Win32-API und COM entwickelt werden, verfügen nicht über ein einheitliches System, wie Fehler ausgelöst und behandelt werden. Einige Methoden der Win32-API verwenden Rückgabewerte, wobei es dem Entwickler obliegt, den Rückgabewert überhaupt auszuwerten und ihn außerdem entsprechend seiner Bedeutung zu interpretieren. Andere Methoden wiederum handhaben die Fehlerbehandlung anders, wobei dies nicht nur von der verwendeten Plattform, sondern zusätzlich noch von der verwendeten Sprache abhängt.

.NET hingegen stellt allen Anwendungen, die für .NET entwickelt werden, ein einheitliches System zur Fehlerbehandlung zur Verfügung. Dieses basiert auf sogenannten Ausnahmen, wobei eine Ausnahme einen konkreten Fehlerfall darstellt. Ein wesentlicher Unterschied zwischen Ausnahmen und den klassischen Rückgabewerten liegt darin, wie sie behandelt werden.

Während es früher Aufgabe des Entwicklers war, auf die Behandlung zu achten, brechen Ausnahmen die Ausführung der Anwendung ab. Damit dies jedoch nicht bei jeder Ausnahme geschieht, bietet C# entsprechende Möglichkeiten, auf Ausnahmen zu reagieren, so dass die Ausführung nach der Fehlerbehandlung fortgesetzt werden kann - erfolgt jedoch keine Fehlerbehandlung, so wird die Ausführung der Anwendung abgebrochen. Es ist also nicht mehr möglich, Fehler zu ignorieren.

Ausnahmen können in .NET so wohl von der Common Language Runtime ausgelöst werden, wenn eine Anwendung beispielsweise versucht, auf eine nicht vorhandene Ressource zuzugreifen, sie können aber auch vom Entwickler gezielt eingesetzt werden, um Fehlersituationen innerhalb der Anwendung zu kennzeichnen.

Damit der fehlerbehandelnde Code auf eine Ausnahme möglichst geeignet reagieren kann, enthalten Ausnahmen neben einer ausführlichen, detaillierten Fehlermeldung auch den sogenannten Aufrufstapel, mit dessen Hilfe sich nachverfolgen lässt, an welcher Stelle in der Ausführung sich die Anwendung gerade befindet. Dabei enthält der Aufrufstapel nicht nur Informationen zu der Klasse, Methode und Zeile, welche die Ausnahme ausgelöst hat, sondern auch zur Aufrufhierarchie.

Des weiteren enthält eine Ausnahme unter Umständen noch weitere, sogenannte innere Ausnahmen, wenn beispielsweise während der Fehlerbehandlung ein weiterer Fehler aufgetreten ist, allerdings Informationen zu beiden Fehlern an die nächste Fehlerbehandlung weitergereicht werden sollen.

Ausnahmen behandeln

Prinzipiell werden Ausnahmen immer dort behandelt, wo sie auftreten. Das heißt, tritt eine Ausnahme innerhalb einer Methode auf, obliegt es dieser Methode, sich um die Fehlerbehandlung zu kümmern. Geschieht dies nicht, so wird die Ausnahme an die aufrufende Methode weitergereicht, die sich ihrerseits nun um die Fehlerbehandlung kümmern kann.

Geschieht auch dies nicht, wird die Ausnahme wieder eine Ebene nach oben gereicht, bis sich entweder eine Methode findet, welche die Ausnahme behandelt, oder die oberste Ebene, also die Main-Methode, erreicht ist. Wird die Ausnahme auch dort nicht behandelt, wird die Ausführung der Anwendung abgebrochen und .NET gibt die Fehlermeldung der Ausnahme an den Benutzer aus.

Um eine Ausnahme abzufangen, bietet C# die beiden Schlüsselwörter try und catch. Beide verfügen über einen Rumpf, der durch geschweifte Klammern eingeschlossen wird. Während try die Anweisungen umschließt, die potenziell eine Ausnahme auslösen könnten, stellt catch den fehlerbehandelnden Code zur Verfügung.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            try
            {
                // Define two operands.
                int operand1 = 23;
                int operand2 = 0;

                // Cause an exception.
                int result = operand1 / operand2;

                // Print the result to the console.
                Console.WriteLine(
                    "The result is " + result + ".");
            }
            catch
            {
                // Catch any exceptions.
                Console.WriteLine("Division by zero!");
            }
        }
    }
}
Im vorangegangenen Beispiel löst die Zeile, in der versucht wird, den einen Operanden durch den anderen zu teilen, eine Ausnahme aus, da die Division durch Null mathematisch nicht definiert ist. Die Ausführung innerhalb des try-Blocks wird daraufhin abgebrochen, weshalb die Ausgabe des Ergebnisses nicht erfolgt. Statt dessen verzweigt die Ausführung in den catch-Block, der eine entsprechende Fehlermeldung ausgibt.

Ein solcher catch-Block reagiert allerdings nicht nur auf die aufgetretene DivideByZeroException, sondern auf sämtliche Ausnahmen. Unter Umständen ist dieses Verhalten allerdings nicht gewünscht, da nur gezielt einige Ausnahmen behandelt werden sollen. Dazu ist es möglich, den Typ der zu behandelnden Ausnahme als Parameter anzugeben.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            try
            {
                // Define two operands.
                int operand1 = 23;
                int operand2 = 0;

                // Cause an exception.
                int result = operand1 / operand2;

                // Print the result to the console.
                Console.WriteLine(
                    "The result is " + result + ".");
            }
            catch (DivideByZeroException)
            {
                // Catch a DivideByZeroException.
                Console.WriteLine("Division by zero!");
            }
        }
    }
}
Derzeit ist es in C# allerdings nicht möglich, mehrere Typen anzugeben. Sollen also mehrere Ausnahmen behandelt werden, müssen mehrere catch-Blöcke verwendet werden.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            try
            {
                // Define two operands.
                int operand1 = 23;
                int operand2 = 0;

                // Cause an exception.
                int result = operand1 / operand2;

                // Print the result to the console.
                Console.WriteLine(
                    "The result is " + result + ".");
            }
            catch (DivideByZeroException)
            {
                // Catch a DivideByZeroException.
                Console.WriteLine("Division by zero!");
            }
            catch (OverflowException)
            {
                // Catch an OverflowException.
                Console.WriteLine("Result too large!");
            }
        }
    }
}
Die einzige Möglichkeit, diese Einschränkung zu umgehen, ist, eine gemeinsame Basisklasse als Typ anzugeben, sofern eine solche existiert. Prinzipiell leiten alle Ausnahmen von der Klasse System.Exception ab, manche verfügen allerdings über eine andere Basisklasse, die ihrerseits erst von System.Exception ableitet. Ein typisierter catch-Block behandelt also nicht nur die Ausnahmen, die dem angegebenen Typ entsprechen, sondern auch all jene, die von diesem Typ abgeleitet sind.

Generell gilt allerdings, dass Ausnahmen so lokal und so spezifisch wie möglich behandelt werden sollten.

Sofern mehrere catch-Blöcke vorhanden sind, muss deren Reihenfolge beachtet werden. Da C# immer den frühesten passenden catch-Block mit der Fehlerbehandlung betraut, ist es wichtig, Blöcke für spezifische Ausnahmen vor solchen für allgemeinere Ausnahmen zu positionieren.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            try
            {
                // Define two operands.
                int operand1 = 23;
                int operand2 = 0;
                
                // Cause an exception.
                int result = operand1 / operand2;

                // Print the result to the console.
                Console.WriteLine(
                    "The result is " + result + ".");
            }
            catch
            {
                // This block is executed on every exception
                // since it catches any exception.
            }
            catch(DivideByZeroException)
            {
                // This block is executed never.
            }
        }
    }
}
Eine Fähigkeit von Ausnahmen wurde noch nicht vorgestellt: Der Zugriff auf die in einer Ausnahme enthaltenen Informationen wie Fehlermeldung, Aufrufstapel und innere Ausnahmen. Dazu ist es nötig, eine Ausnahme mit einem Variablennamen zu kennzeichnen, so dass darauf innerhalb des catch-Blocks zugegriffen werden kann. Es hat sich in der Praxis eingebürgert, Ausnahmen mit der Abkürzung ex zu benennen, obwohl dies nicht den Namenskonventionen für lokale Variablen entspricht, weshalb diese Bezeichnung in den folgenden Beispiele nicht verwendet wird.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            try
            {
                // Define two operands.
                int operand1 = 23;
                int operand2 = 0;

                // Cause an exception.
                int result = operand1 / operand2;

                // Print the result to the console.
                Console.WriteLine(
                    "The result is " + result + ".");
            }
            catch (DivideByZeroException exception)
            {
                // Catch a DivideByZeroException.
                Console.WriteLine(exception.Message);
            }
        }
    }
}
Auch ein Weiterreichen und somit ein erneutes Auslösen einer Ausnahme innerhalb eines catch-Blocks ist möglich, was in C# mit Hilfe des Schlüsselwortes throw geschieht. Es wird kein weiterer Parameter benötigt, da throw immer die Ausnahme weiterreicht, in deren fehlerbehandelndem Block sich der entsprechende Aufruf befindet.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            try
            {
                // Define two operands.
                int operand1 = 23;
                int operand2 = 0;

                // Cause an exception.
                int result = operand1 / operand2;

                // Print the result to the console.
                Console.WriteLine(
                    "The result is " + result + ".");
            }
            catch (DivideByZeroException exception)
            {
                // Catch the DivideByZeroException.
                Console.WriteLine(exception.Message);

                // Rethrow the exception.
                throw;
            }
        }
    }
}
Dennoch kann eine Ausnahme als Parameter angegeben werden, wobei dabei allerdings der Aufrufstapel verloren geht, weshalb dies in der Praxis als schlechter Stil angesehen wird.

In einigen Fällen kann es vorkommen, dass Code im Anschluss an einen try-catch-Block ausgeführt werden muss, unabhängig davon, ob der try-Block vollständig erfolgreich durchlaufen wurde oder nicht, wenn also eine Ausnahme ausgelöst wurde. Solcher Code könnte beispielsweise dazu dienen, eine geöffnete Verbindung zu einer Datenbank zu schließen oder sonstige Ressourcen wieder freizugeben. Im einfachsten Fall genügt es, solchen Code hinter dem catch-Block anzugeben.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
    // TODO gr: Do something.
    //          2008-01-02
}
catch
{
    // TODO gr: Handle eventually thrown exceptions.
    //          2008-01-02
}

// TODO gr: Clean up.
//          2008-01-02
Führt jedoch mindestens einer der beiden Blöcke ein return aus und verlässt die aktuelle Methode damit, oder reicht der catch-Block die Ausnahme an eine höhergelegene Methode weiter, wird der entsprechende Code nicht mehr ausgeführt.

Eine denkbare Lösung wäre, den entsprechenden Code in beiden Blöcken einzufügen, doch dies verschlechtert die Wartbarkeit und erhöht die Unübersichtlichkeit. Statt dessen stellt C# das Schlüsselwort finally zur Verfügung, das einen weiteren Block nach try und catch einleitet, dessen Inhalt in jedem Fall ausgeführt wird - sogar dann, wenn durch einen der beiden Blöcke ein return oder ein throw ausgeführt wird.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try
{
    // TODO gr: Do something.
    //          2008-01-02

    return;
    // Return to the caller.
}
catch
{
    // TODO gr: Handle eventually thrown exceptions.
    //          2008-01-02

    // Rethrow the exception.
    throw;
}
finally
{
    // TODO gr: Clean up.
    //          2008-01-02
}

Benutzerdefinierte Ausnahmen

Wie zu Anfang bereits erwähnt ist es dem Entwickler möglich, eigene Ausnahmen zu definieren, um Fehlerzustände innerhalb der Anwendung zu kennzeichnen. Prinzipiell ist eine solche benutzerdefinierte Ausnahme nichts anderes, als eine direkt oder indirekt von System.Exception abgeleitete Klasse.

Um systembedingte und benutzerdefinierte Ausnahmen unterscheiden zu können, war es bis zur Version 2.0 von .NET in der Praxis üblich, eigene Ausnahmeklassen nicht von System.Exception, sondern von der Klasse System.ApplicationException abzuleiten, die ihrerseits wiederum von System.Exception ableitet. Seit der Version 3.0 von .NET wird die Verwendung der Klasse System.ApplicationException nicht mehr empfohlen.

Ausgelöst wird eine benutzerdefinierte Ausnahme mit Hilfe des bereits bekannten Schlüsselwortes throw, wobei diesem als Parameter eine neue Instanz der entsprechenden Ausnahme übergeben wird.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a custom defined exception.
    /// </summary>
    public class MyException : Exception
    {
    }

    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            // Throw a custom defined exception.
            throw new MyException();
        }
    }
}
Es wird in der Praxis als guter Stil angesehen, die Standardkonstruktoren der Basisklasse System.Exception zu überschreiben, um auch benutzerdefinierte Ausnahmen durch Angabe der entsprechenden Parameter mit einer Fehlermeldung und inneren Ausnahmen ausstatten zu können.

Leistung und Ressourcenbedarf

Im Zusammenhang mit Ausnahmen liest und hört man häufig, dass diese nicht eingesetzt werden sollten, da sie sehr leistungshungrig seien. Aus dieser Aussage ergibt sich direkt die Frage, wann Ausnahmen überhaupt eingesetzt werden sollten.

Prinzipiell ergibt sich die Antwort auf diese Frage bereits aus dem Begriff einer Ausnahme: Sie stellen Ausnahmesituationen dar. Das heißt, Ausnahmen sind explizit nicht dazu gedacht, bedenkenlos an den verschiedensten Stellen innerhalb einer Anwendung eingesetzt zu werden. Sofern es möglich ist, einen Fehler im Vorfeld abzufangen, sollte dies dem Einsatz einer Ausnahme vorgezogen werden.

Beispielsweise würde man in dem Beispiel, das die DivideByZeroException abfängt, in der Praxis keine Ausnahme einsetzen, sondern im Vorfeld mit Hilfe einer if-Abfrage prüfen, ob durch 0 geteilt werden soll. Insbesondere, wenn solche Berechnungen innerhalb von Schleifen auftreten, kann dadurch die Leistung der Anwendung durchaus gesteigert werden.

Dies liegt daran, dass für jede Ausnahme, die ausgelöst wird, der Aufrufstapel ermittelt werden muss, was bei einer entsprechend tiefen Verschachtelung von Methodenaufrufen unter Umständen aufwändig sein kann.

Obwohl Ausnahmen also nicht wahlfrei eingesetzt werden sollten, gibt es dennoch Fälle, in denen ihr Einsatz nicht verzichtbar ist. Dann nämlich, wenn Fehler nicht erwartbar sind und auf Ausnahmesituationen reagiert werden muss. In einem solchen Fall ist es in der Regel allerdings ohnehin nötig, den Benutzer zu informieren und ihn das weitere Vorgehen bestimmen zu lassen, weshalb es in einer solchen Situation nicht darauf ankommt, ob eine Ausnahme schnell oder langsam erzeugt wird - die Anwendung gelangt auf beide Arten zum Stillstand.

Zusammengefasst lässt sich also sagen, dass Ausnahmen entgegen ihrem Ruf durchaus eingesetzt werden können, dass dies allerdings gezielt und mit Bedacht geschehen sollte. Insbesondere sollten Fehlersituationen bereits im Vorfeld vermieden werden, sofern dies möglich ist.