Freigeben über


Best Practices für Ausnahmen

Die ordnungsgemäße Ausnahmebehandlung ist für die Anwendungssicherheit unerlässlich. Sie können absichtlich erwartete Ausnahmen behandeln, um zu verhindern, dass Ihre App abstürzt. Eine abgestürzte App ist jedoch zuverlässiger und diagnosefähiger als eine App mit nicht definiertem Verhalten.

In diesem Artikel werden Best Practices für die Behandlung und Erstellung von Ausnahmen beschrieben.

Ausnahmen behandeln

Die folgenden bewährten Methoden betreffen die Behandlung von Ausnahmen:

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

Verwenden Sie für Code, der potenziell eine Ausnahme generieren kann und wenn Ihre App aus dieser Ausnahme wiederhergestellt kann, try/catch-Blöcke um den Code herum. 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 allgemeiner Bedingungen zum Vermeiden 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 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 ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

Die Wahl des Ansatzes 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.

    Hinweis

    Vorabüberprüfungen beseitigen die meisten Ausnahmen. Es kann jedoch Racebedingungen geben, bei denen sich die überwachte Bedingung zwischen der Prüfung und dem Betrieb ändert, und in diesem Fall können dennoch Ausnahmen verursacht werden.

Aufrufen von Try*-Methoden zum Vermeiden von Ausnahmen

Wenn die Leistungskosten von Ausnahmen unerschwinglich sind, stellen einige .NET-Bibliotheksmethoden alternative Formen der Fehlerbehandlung bereit. Beispielsweise löst Int32.Parse eine OverflowException aus, wenn der zu analysierende Wert zu groß ist, um durch Int32 dargestellt zu werden. Int32.TryParse löst diese Ausnahme jedoch nicht aus. Stattdessen wird ein boolescher Wert zurückgegeben, der einen out-Parameter enthält, der die analysierte gültige ganze Zahl nach erfolgreicher Durchführung enthält. Dictionary<TKey,TValue>.TryGetValue hat ein ähnliches Verhalten für den Versuch, einen Wert aus einem Wörterbuch abzurufen.

Abbruch und asynchrone Ausnahmen abfangen

Es ist besser, OperationCanceledException anstelle von TaskCanceledException, das von OperationCanceledException abgeleitet wird, abzufangen, wenn Sie eine asynchrone Methode aufrufen. Viele asynchrone Methoden lösen eine OperationCanceledException-Ausnahme aus, wenn ein Abbruch angefordert wird. Diese Ausnahmen ermöglichen es, die Ausführung effizient anzuhalten, und der Aufrufstapel wird nach der Beobachtung einer Abbruchanforderung aufgehoben.

Asynchrone Methoden speichern Ausnahmen, die während der Ausführung in der zurückgegebenen Aufgabe ausgelöst werden. Wenn eine Ausnahme in der zurückgegebenen Aufgabe gespeichert wird, wird diese Ausnahme ausgelöst, wenn die Aufgabe erwartet wird. Verwendungsausnahmen, z. B. ArgumentException, werden weiterhin synchron ausgelöst. Weitere Informationen finden Sie unter Asynchronen Ausnahmen.

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 die FileStream-Klasse Methoden bereit, mithilfe derer bestimmt werden kann, ob das Ende der Datei erreicht wurde. Sie können diese Methoden aufrufen, um die Ausnahme zu vermeiden, die ausgelöst wird, wenn Sie über das Ende der Datei hinaus gelesen haben. Das folgende Beispiel zeigt, wie eine Datei bis zum Ende gelesen werden kann, ohne dass eine Ausnahme ausgelöst wird.

class FileRead
{
    public static void ReadAll(FileStream fileToRead)
    {
        ArgumentNullException.ThrowIfNull(fileToRead);

        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.

Berücksichtigen Sie bei Werttypen, ob sie Nullable<T> oder default als Fehlerindikator für Ihre App verwenden möchten. 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.

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

Ordnungsgemäßes Erfassen und erneutes Erfassen von Ausnahmen

Wenn eine Ausnahme ausgelöst wird, ist ein Teil der darin enthaltenen Informationen die Stapelüberwachung. Die Stapelüberwachung ist eine Liste der Hierarchie der Methodenaufrufe, die mit der Methode beginnt, die die Ausnahme auslöst, und mit der Methode endet, die die Ausnahme abfängt. Wenn Sie eine Ausnahme durch Angabe der Ausnahme in der throw-Anweisung erneut auslösen, beispielsweise throw e, startet die Stapelüberwachung bei der aktuellen Methode neu. Die Liste der Methodenaufrufe zwischen der ursprünglichen Methode,die Ausnahme ausgelöst hat, und der aktuellen Methode geht verloren. Um die ursprünglichen Stapelablaufverfolgungsinformationen mit der Ausnahme beizubehalten, gibt es zwei Optionen, die davon abhängen, wo Sie die Ausnahme erneut drosseln:

  • Wenn Sie die Ausnahme innerhalb des Handlers wiederholen (catch-Block), der die Ausnahmeinstanz abgefangen hat, verwenden Sie die throw-Anweisung, ohne die Ausnahme anzugeben. Mithilfe der Codeanalyseregel CA2200 können Sie Orte in Ihrem Code finden, an denen versehentlich Stapelablaufverfolgungsinformationen verloren gehen.
  • Wenn Sie die Ausnahme von einer anderen Stelle als dem Handler (catch Block) erneut entdrosseln, verwenden ExceptionDispatchInfo.Capture(Exception) Sie, um die Ausnahme im Handler zu erfassen, und ExceptionDispatchInfo.Throw() wenn Sie sie erneut einfangen möchten. Sie können die ExceptionDispatchInfo.SourceException-Eigenschaft verwenden, um die erfasste Ausnahme zu überprüfen.

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

Auslösen von Ausnahmen

Die folgenden bewährten Methoden betreffen das Auslösen von Ausnahmen:

Verwenden vordefinierter 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.

Hinweis

Es ist zwar am besten, vordefinierte Ausnahmetypen nach Möglichkeit zu verwenden, sie sollten jedoch keine reservierten Ausnahmetypen auslösen, z. B. AccessViolationException, IndexOutOfRangeException, NullReferenceException und StackOverflowException. Weitere Informationen finden Sie unter CA2201: Keine reservierten Ausnahmetypen auslösen.

Verwenden von Methoden zum Generieren von Ausnahmen

Häufig löst eine Klasse die jeweils gleiche Ausnahme an unterschiedlichen Stellen in der Implementierung aus. Um übermäßigen Code zu vermeiden, erstellen Sie eine Hilfsmethode, welche die Ausnahme erstellt und zurückgibt. Zum Beispiel:

class FileReader
{
    private readonly string _fileName;

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

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

    static 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

Einige wichtige .NET-Ausnahmetypen weisen solche statischen throw-Hilfsmethoden auf, welche die Ausnahme zuordnen und auslösen. Sie sollten diese Methoden aufrufen, anstatt den entsprechenden Ausnahmetyp zu erstellen und auszulösen:

Tipp

Mithilfe der folgenden Codeanalyseregeln können Sie Orte in Ihrem Code finden, an denen Sie diese statischen throw-Hilfsprogramme nutzen können: CA1510, CA1511, CA1512und CA1513.

Wenn Sie eine asynchrone Methode implementieren, rufen Sie CancellationToken.ThrowIfCancellationRequested() auf, anstatt zu überprüfen, ob der Abbruch angefordert wurde, und erstellen Sie OperationCanceledException und lösen Sie sie aus. Weitere Informationen finden Sie unter CA2250.

Einschließen einer lokalisierten Zeichenfolgenmeldung

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:

Verwenden richtiger Grammatik

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. Beispielsweise verwendet „Die Protokolltabelle ist übergelaufen.“ die richtige Grammatik und Interpunktion.

Gutes Platzieren von Throw-Anweisungen

Platzieren Sie 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.

Keine Ausnahmen in finally-Klauseln auslösen

Lösen Sie keine Ausnahmen in finally-Klauseln aus. Weitere Informationen finden Sie unter Codeanalyseregel CA2219.

Keine Ausnahmen aus unerwarteten Orten auslösen

Einige Methoden wie Equals-, GetHashCode- und ToString-Methoden, statische Konstruktoren und Gleichheitsoperatoren sollten keine Ausnahmen auslösen. Weitere Informationen finden Sie unter Codeanalyseregel CA1065.

Synchrones Auslösen von Argumentüberprüfungsausnahmen

In Aufgabenrückgabemethoden sollten Sie Argumente überprüfen und entsprechende Ausnahmen auslösen, z. B. ArgumentException und ArgumentNullException, bevor Sie in den asynchronen Teil der Methode übergehen. Ausnahmen, die im asynchronen Teil der Methode ausgelöst werden, werden in der zurückgegebenen Aufgabe gespeichert und erst angezeigt, wenn beispielsweise die Aufgabe erwartet wird. Weitere Informationen finden Sie unter Ausnahmen in Aufgabenrückgabemethoden.

Benutzerdefinierte Ausnahmetypen

Die folgenden bewährten Methoden betreffen benutzerdefinierte Ausnahmetypen:

Klassennamen der Ausnahme mit Exception abschließen

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

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

Einfügen von drei Konstruktoren

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 Erstellen benutzerdefinierter Ausnahmen.

Bereitstellen zusätzlicher Eigenschaften nach Bedarf

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.

Weitere Informationen