Best Practices für Ausnahmen

Eine ausgereifte App behandelt Ausnahmen und Fehler, um App-Abstürze zu verhindern. In diesem Artikel werden Best Practices für die Behandlung und Erstellung von Ausnahmen beschrieben.

Verwenden Sie Try/Catch/Finally-Blöcke zum Beheben von Fehlern oder zum Freigeben von Ressourcen.

Betten Sie Code, der Ausnahmen erzeugen kann, in try/catch-Blöcke ein, damit ihr Code nach diesen Ausnahmen wiederhergestellt werden kann. Ordnen Sie Ausnahmen in catch-Blöcken immer von der am stärksten abgeleiteten zur am wenigsten abgeleiteten. Alle Ausnahmen sind von der Exception-Klasse abgeleitet. Stärker abgeleitete Ausnahmen werden nicht durch eine catch-Klausel verarbeitet, der eine catch-Klausel für eine Ausnahmebasisklasse vorangestellt ist. Wenn Ihr Code aus einer Ausnahme nicht wiederhergestellt werden kann, fangen Sie diese Ausnahme nicht ab. Aktivieren Sie Methoden weiter oben in der Aufrufliste, um nach Möglichkeit eine Wiederherstellung auszuführen.

Bereinigen Sie die Ressourcen, die mit using-Anweisungen oder finally-Blöcken zugeordnet wurden. Bevorzugen Sie using-Anweisungen, um Ressourcen automatisch zu bereinigen, wenn Ausnahmen ausgelöst werden. Verwenden Sie finally-Blöcke, um Ressourcen zu bereinigen, die IDisposable nicht implementieren. Code in einer finally-Klausel wird fast immer – d. h. auch bei Auslösen einer Ausnahme – ausgeführt.

Behandeln häufig auftretender Bedingungen ohne Auslösen von Ausnahmen

Bedingungen, deren Auftreten wahrscheinlich ist, die aber möglicherweise eine Ausnahme auslösen, sollten Sie so behandeln, dass die Ausnahme vermieden wird. Wenn Sie z. B. versuchen, eine bereits geschlossene Verbindung erneut zu schließen, erhalten Sie eine InvalidOperationException. Dies können Sie verhindern, indem Sie eine if-Anweisung verwenden, um den Verbindungsstatus zu überprüfen, bevor Sie versuchen, die Verbindung zu schließen.

if (conn->State != ConnectionState::Closed)
{
    conn->Close();
}
if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

Wenn Sie den Verbindungsstatus vor dem Schließen der Verbindung nicht überprüfen, können Sie die InvalidOperationException abfangen.

try
{
    conn->Close();
}
catch (InvalidOperationException^ ex)
{
    Console::WriteLine(ex->GetType()->FullName);
    Console::WriteLine(ex->Message);
}
try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

Die Wahl der Methode hängt von der erwarteten Häufigkeit des Ereignisses ab.

  • Verwenden Sie die Ausnahmebehandlung, wenn das Ereignis nicht sehr häufig auftritt, d. h. wenn es wirklich ungewöhnlich ist und auf einen Fehler hinweist (z. B. ein unerwartetes Dateiende). Wenn Sie die Ausnahmebehandlung verwenden, wird weniger Code in normalen Bedingungen ausgeführt.

  • Wenn das Ereignis regelmäßig auftritt und als Teil der normalen Programmausführung betrachtet werden kann, suchen Sie im Code nach Fehlerbedingungen. Wenn Sie auf gängige Fehlerbedingungen prüfen, wird weniger Code ausgeführt, da Ausnahmen vermieden werden.

Entwerfen von Klassen mit dem Ziel, Ausnahmen zu vermeiden

Eine Klasse kann Methoden oder Eigenschaften bereitstellen, mit deren Hilfe ein Aufruf vermieden werden kann, der andernfalls eine Ausnahme auslösen würde. Beispielsweise stellt eine FileStream-Klasse Methoden bereit, mithilfe derer bestimmt werden kann, ob das Ende der Datei erreicht wurde. Dadurch kann die Ausnahme vermieden werden, die andernfalls durch den Versuch ausgelöst würde, nach Erreichen des Dateiendes weiterzulesen. Das folgende Beispiel zeigt, wie eine Datei bis zum Ende gelesen werden kann, ohne dass eine Ausnahme ausgelöst wird.

class FileRead
{
public:
    void ReadAll(FileStream^ fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == nullptr)
        {
            throw gcnew System::ArgumentNullException();
        }

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead->Seek(0, SeekOrigin::Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead->Length; i++)
        {
            b = fileToRead->ReadByte();
            Console::Write(b.ToString());
            // Or do something else with the byte.
        }
    }
};
class FileRead
{
    public void ReadAll(FileStream fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == null)
        {
            throw new ArgumentNullException();
        }

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

Eine andere Möglichkeit zum Vermeiden von Ausnahmen besteht darin, bei sehr häufig auftretenden Fehlern NULL (oder einen Standardwert) zurückzugeben, statt eine Ausnahme auszulösen. Ein sehr häufig auftretender Fehler kann durchaus als normale Ablaufsteuerung betrachtet werden. Indem Sie in diesen Fällen NULL (oder einen Standardwert) zurückgeben, minimieren Sie die Auswirkungen auf die Leistung einer App.

Ob Sie bei Werttypen Nullable<T> oder einen Standardwert als Fehlerindikator verwenden, richtet sich nach Ihrer App. Durch Verwendung von Nullable<Guid> wird default zu null statt zu Guid.Empty. Manchmal wird durch Hinzufügen von Nullable<T> klarer, ob ein Wert vorhanden oder nicht vorhanden ist. Andererseits kann das Hinzufügen von Nullable<T> dazu führen, dass zusätzliche Fälle geprüft werden müssen, die eigentlich nicht notwendig sind und nur zu potenzielle Fehlerquellen führen.

Auslösen von Ausnahmen statt Zurückgeben eines Fehlercodes

Ausnahmen sorgen dafür, dass Fehler nicht unbemerkt bleiben, nur weil der aufrufende Code einen Rückgabecode nicht überprüft hat.

Verwenden der vordefinierten .NET-Ausnahmetypen

Verwenden Sie eine neue Ausnahmeklasse nur dann, wenn sich keine vordefinierte Klasse anbietet. Zum Beispiel:

  • Wenn ein Eigenschaftssatz oder ein Methodenaufruf angesichts des aktuellen Zustands des Objekts nicht angemessen ist, werfen Sie eine InvalidOperationException-Ausnahme.
  • Wenn ungültige Parameter übergeben werden, lösen Sie eine ArgumentException-Ausnahme oder eine der vordefinierten Klassen aus, die von ArgumentException abgeleitet sind.

Verwenden des Worts Exception am Ende von Ausnahmeklassennamen

Wenn eine benutzerdefinierte Ausnahme erforderlich ist, benennen Sie diese entsprechend, und leiten Sie sie von der Exception-Klasse ab. Beispiel:

public ref class MyFileNotFoundException : public Exception
{
};
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Einschließen von drei Konstruktoren in benutzerdefinierte Ausnahmeklassen

Verwenden Sie beim Erstellen eigener Ausnahmeklassen mindestens die drei gängigen Konstruktoren: den parameterlosen Konstruktor, einen Konstruktor, der eine Zeichenfolgenmeldung entgegennimmt, und einen Konstruktor, der eine Zeichenfolgenmeldung und eine innere Ausnahme entgegennimmt.

Ein Beispiel finden Sie unter Gewusst wie: Erstellen benutzerdefinierter Ausnahmen.

Sicherstellen, dass Ausnahmedaten bei der Remoteausführung von Code verfügbar sind

Stellen Sie beim Erstellen benutzerdefinierter Ausnahmen sicher, dass die Metadaten für die Ausnahmen für remote ausgeführten Code verfügbar sind.

In .NET-Implementierungen, die App-Domänen unterstützen, können Ausnahmen beispielsweise über mehrere App-Domänen hinweg auftreten. Nehmen wir an, App-Domäne A erstellt App-Domäne B, und in App-Domäne B wird Code ausgeführt, der eine Ausnahme auslöst. Damit App-Domäne A die Ausnahme ordnungsgemäß abfängt und behandelt, muss sie die Assembly finden, in der die durch App-Domäne B ausgelöste Ausnahme enthalten ist. Wenn App-Domäne B eine Ausnahme auslöst, die in einer Assembly in ihrer Anwendungsbasis, nicht aber in der Anwendungsbasis von App-Domäne A enthalten ist, kann App-Domäne A die Ausnahme nicht finden. Daraufhin löst die Common Language Runtime eine FileNotFoundException-Ausnahme aus. Um diese Situation zu vermeiden, haben Sie zwei Möglichkeiten, die Assembly mit den Ausnahmeinformationen bereitzustellen:

  • Sie legen die Assembly in einer gemeinsamen Anwendungsbasis ab, die von beiden App-Domänen verwendet wird.
  • Sofern die Domänen keine gemeinsame Anwendungsbasis verwenden, signieren Sie die Assembly, in der die Ausnahmeinformationen enthalten sind, mit einem starken Namen und legen sie in einem globalen Assemblycache ab.

Verwenden grammatisch korrekter Fehlermeldungen

Verfassen Sie klar verständliche Meldungen und achten Sie vor allem am Ende eines Satzes auf korrekte Satzzeichen. Jeder Satz in der Zeichenfolge, der der Exception.Message-Eigenschaft zugeordnet ist, muss mit einem Punkt enden. Ein Beispiel für eine geeignete Beschreibungszeichenfolge ist: „Es ist ein Überlauf der Protokolltabelle aufgetreten."

Einschließen einer lokalisierten Meldungszeichenfolge in jede Ausnahme

Die dem Benutzer angezeigte Fehlermeldung wird von der Exception.Message-Eigenschaft der ausgelösten Ausnahme abgeleitet, nicht vom Namen der Ausnahmeklasse. In der Regel können Sie der Exception.Message-Eigenschaft einen Wert zuweisen, indem Sie die Meldungszeichenfolge an das message-Argument eines Ausnahmekonstruktors übergeben.

Sie sollten für lokalisierte Anwendungen eine lokalisierte Meldungszeichenfolge für jede Ausnahme angeben, die Ihre Anwendung ausgeben könnte. Verwenden Sie Ressourcendateien, um lokalisierte Fehlermeldungen zur Verfügung zu stellen. Weitere Informationen zum Lokalisieren von Anwendungen und zum Abrufen lokalisierter Zeichenfolgen finden Sie in den folgenden Artikeln:

Bereitstellen zusätzlicher Eigenschaften in benutzerdefinierten Ausnahmen, sofern erforderlich

Geben Sie zusätzliche Eigenschaften für eine Ausnahme nur dann neben der benutzerdefinierten Meldungszeichenfolge an, wenn es ein programmgesteuertes Szenario gibt, in dem ein solcher Zusatz sinnvoll ist. Beispielsweise gibt die FileNotFoundException die FileName-Eigenschaft an.

Platzieren von throw-Anweisungen in einer Weise, dass die Stapelüberwachung nützlich ist

Die Stapelüberwachung beginnt bei der Anweisung, bei der die Ausnahme ausgelöst wurde, und endet mit der catch-Anweisung, mit der die Ausnahme abgefangen wird.

Verwenden von Methoden zum Generieren von Ausnahmen

Häufig löst eine Klasse die jeweils gleiche Ausnahme an unterschiedlichen Stellen in der Implementierung aus. Verwenden Sie Hilfsmethoden, die eine Ausnahme erstellen und zurückgeben, um ausufernden Code zu vermeiden. Zum Beispiel:

ref class FileReader
{
private:
    String^ fileName;

public:
    FileReader(String^ path)
    {
        fileName = path;
    }

    array<Byte>^ Read(int bytes)
    {
        array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
        if (results == nullptr)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException^ NewFileIOException()
    {
        String^ description = "My NewFileIOException Description";

        return gcnew FileReaderException(description);
    }
};
class FileReader
{
    private string fileName;

    public FileReader(string path)
    {
        fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(fileName, bytes);
        if (results == null)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

In einigen Fällen ist es besser, eine Ausnahme mithilfe des zugehörigen Konstruktors zu erstellen. Ein Beispiel hierfür ist eine globale Ausnahmeklasse wie etwa ArgumentException.

Wiederherstellen des Status, wenn Methoden aufgrund von Ausnahmen nicht abgeschlossen werden

Aufrufende Funktionen sollten erwarten können, dass beim Auslösen einer Ausnahme durch eine Methode keine Nebeneffekte auftreten. Angenommen, Sie haben Code für Geldüberweisungen geschrieben. Ihr Code bucht einen Betrag von einem Konto ab und schreibt ihn einem anderen Konto gut. Wenn nun beim Ausführen der Gutschrift eine Ausnahme ausgelöst wird, darf die Abbuchung natürlich nicht bestehen bleiben.

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

Die vorherige Methode löst keine direkten Ausnahmen aus. Sie müssen jedoch die Methode schreiben, damit die Auszahlung rückgängig gemacht wird, wenn der Einzahlungsvorgang fehlschlägt.

In dieser Situation besteht eine Möglichkeit darin, alle Ausnahmen abzufangen, die von der Gutschrifttransaktion ausgelöst wurden, und für die Abbuchung einen Rollback auszuführen.

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

Dieses Beispiel veranschaulicht die Verwendung von throw, um die ursprüngliche Ausnahme erneut auszulösen, damit aufrufende Funktionen die tatsächliche Ursache des Problems erkennen können, ohne die InnerException-Eigenschaft untersuchen zu müssen. Eine Alternative besteht darin, eine neue Ausnahme auszulösen und die ursprüngliche Ausnahme als innere Ausnahme einzuschließen:

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

Erfassen von Ausnahmen, die später erneut ausgeführt werden sollen

Verwenden Sie die System.Runtime.ExceptionServices.ExceptionDispatchInfo -Klasse, um eine Ausnahme zu erfassen und den Aufrufstapel beizubehalten, um sie später erneut erstellen zu können. Diese Klasse stellt die folgenden Methoden und Eigenschaften (unter anderem) bereit:

Das folgende Beispiel zeigt, wie die ExceptionDispatchInfo-Klasse verwendet werden kann und wie die Ausgabe aussehen könnte.

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

Wenn die Datei im Beispielcode nicht vorhanden ist, wird die folgende Ausgabe erzeugt:

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

Siehe auch