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.