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.
5.1 Was ist falsch an Rückgabecodes?
 
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.
5.2 try und catch
 
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.
5.3 Die Ausnahmehierarchie
 
Alle C#-Ausnahmen leiten sich aus der Klasse Exception ab, die Teil der Common Language Runtime
ist1 . 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.
5.4 Übergeben von Ausnahmen an die aufrufende Funktion
 
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.
5.4.1 Caller Beware
 
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.
5.4.2 Caller Confuse
 
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.
5.4.3 Caller Inform
 
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.
5.5 Benutzerdefinierte Ausnahmeklassen
 
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.
5.6 Finally
 
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.
5.7 Effizienz und Overhead
 
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.
5.8 Entwurfsrichtlinien
 
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.
|