Freigeben über


Bewährte Methoden 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 bewährte Methoden zum Behandeln und Erstellen von Ausnahmen beschrieben.

Behandeln von Ausnahmen

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. In catch Blöcken sortieren Sie immer Ausnahmen von den am meisten abgeleiteten zu den am wenigsten abgeleiteten. (Alle Ausnahmen werden von der Exception Klasse abgeleitet. Weitere abgeleitete Ausnahmen werden nicht von einer catch Klausel behandelt, die einer catch Klausel für eine Basis ausnahmeklasse 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 ausgeführt, auch wenn Ausnahmen ausgelöst werden.

Umgang mit häufigen Bedingungen zur Vermeidung von Ausnahmen

Bei Bedingungen, die wahrscheinlich auftreten, aber eine Ausnahme auslösen können, sollten Sie sie auf eine Weise behandeln, die die Ausnahme verhindert. Wenn Sie beispielsweise versuchen, eine bereits geschlossene Verbindung zu schließen, erhalten Sie eine InvalidOperationException. Sie können dies vermeiden, indem Sie eine if Anweisung verwenden, um den Verbindungsstatus zu überprüfen, bevor Sie versuchen, ihn 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 nicht überprüfen, können Sie die InvalidOperationException Ausnahme 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

Der Ansatz zur Auswahl hängt davon ab, wie oft das Ereignis Ihrer Erwartung nach auftritt.

  • Verwenden Sie die Ausnahmebehandlung, wenn das Ereignis nicht häufig auftritt, d. h., wenn das Ereignis wirklich außergewöhnlich ist und einen Fehler angibt, z. B. ein unerwartetes Ende der Datei. Wenn Sie die Ausnahmebehandlung verwenden, wird weniger Code unter normalen Bedingungen ausgeführt.

  • Überprüfen Sie im Code auf Fehlerbedingungen, wenn das Ereignis routinemäßig auftritt und als Teil der normalen Ausführung betrachtet werden kann. Wenn Sie nach allgemeinen Fehlerbedingungen suchen, wird weniger Code ausgeführt, da Sie Ausnahmen vermeiden.

    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.

Rufen Sie Try*-Methoden auf, um Ausnahmen zu vermeiden

Wenn die Leistungskosten von Ausnahmen unerschwinglich sind, stellen einige .NET-Bibliotheksmethoden alternative Formen der Fehlerbehandlung bereit. Int32.Parse wirft einen OverflowException aus, wenn der zu analysierende Wert zu groß ist, um durch Int32 dargestellt zu werden. Jedoch löst Int32.TryParse diese Ausnahme nicht aus. Stattdessen gibt die Funktion einen Booleschen Wert zurück und besitzt einen out Parameter, der die analysierte gültige ganze Zahl bei Erfolg 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 abzufangen, anstatt TaskCanceledException, das von OperationCanceledException abgeleitet wird, 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 abgewartet wird. Verwendungsausnahmen, z. B. ArgumentException, werden weiterhin synchron ausgelöst. Weitere Informationen finden Sie unter "Asynchrone Ausnahmen".

Entwerfen von Klassen, sodass Ausnahmen vermieden werden können

Eine Klasse kann Methoden oder Eigenschaften bereitstellen, mit denen Sie vermeiden können, einen Aufruf zu tätigen, der eine Ausnahme auslösen würde. Die Klasse stellt z. B. Methoden bereit, mit denen ermittelt wird, FileStream 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 gelesen haben. Das folgende Beispiel zeigt, wie Sie das Ende einer Datei lesen, ohne eine Ausnahme auszulösen:

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 weitere Möglichkeit, Ausnahmen zu vermeiden, ist die Rückgabe null (oder Standardeinstellung) für die häufigsten Fehlerfälle, anstatt eine Ausnahme auszuwerfen. Ein häufiger Fehlerfall kann als normaler Kontrollfluss betrachtet werden. Durch Zurückgeben null (oder Standard) in diesen Fällen minimieren Sie die Leistungseinbußen für eine App.

Berücksichtigen Sie bei Werttypen, ob Sie Nullable<T> oder default als Fehlerindikator für Ihre App verwenden möchten. Durch die Verwendung von Nullable<Guid> wird default zu null anstatt zu Guid.Empty. Das Hinzufügen Nullable<T> kann es manchmal deutlicher machen, wenn ein Wert vorhanden oder nicht vorhanden ist. In anderen Fällen kann das Hinzufügen Nullable<T> zusätzliche Fälle erstellen, um zu überprüfen, ob dies nicht erforderlich ist, und dient nur zum Erstellen potenzieller Fehlerquellen.

Zustand wiederherstellen, 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. Wenn Sie zum Beispiel Code haben, der Geld überweist, indem er von einem Konto abhebt und auf ein anderes Konto einzahlt, und eine Ausnahme während der Ausführung der Einzahlung ausgelöst wird, möchten Sie nicht, dass die Abhebung bestehen bleibt.

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 vorstehende Methode löst keine Ausnahmen direkt aus. Sie müssen die Methode jedoch so schreiben, dass die Auszahlung rückgängig gemacht wird, wenn der Einzahlungsvorgang fehlschlägt.

Eine Möglichkeit, diese Situation zu behandeln, besteht darin, Ausnahmen abzufangen, die von der Einzahlungstransaktion ausgelöst werden, und die Auszahlung zurückzunehmen.

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

In diesem Beispiel wird gezeigt, wie mit throw die ursprüngliche Ausnahme erneut ausgelöst wird, damit die Aufrufer die tatsächliche Ursache des Problems leichter erkennen können, ohne die InnerException Eigenschaft untersuchen zu müssen. Eine Alternative besteht darin, eine neue Ausnahme auszuwerfen 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 erneut auslösen, indem Sie beispielsweise die Ausnahme in der throw Anweisung angeben, throw ewird die Stapelablaufverfolgung bei der aktuellen Methode neu gestartet und die Liste der Methodenaufrufe zwischen der ursprünglichen Methode, die die Ausnahme ausgelöst hat, und die aktuelle 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 prüfen.

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

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 erstellt:

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

Führen Sie eine neue Ausnahmeklasse nur ein, wenn eine vordefinierte Klasse nicht angewendet wird. Beispiel:

  • Wenn ein Eigenschaftensatz- oder Methodenaufruf aufgrund des aktuellen Zustands des Objekts nicht geeignet ist, lösen Sie eine InvalidOperationException Ausnahme aus.
  • Wenn ungültige Parameter übergeben werden, sollte man eine ArgumentException-Ausnahme oder eine der vordefinierten Klassen, die von ArgumentException erben, auslösen.

Hinweis

Obwohl es am besten ist, vordefinierte Ausnahmetypen nach Möglichkeit zu verwenden, sollten Sie nicht einige reservierte Ausnahmetypen auslösen, wie AccessViolationException, IndexOutOfRangeException, NullReferenceException und StackOverflowException. Weitere Informationen finden Sie unter CA2201: Keine reservierten Ausnahmetypen auslösen.

Verwenden von Ausnahme-Generator-Methoden

Es ist üblich, dass eine Klasse dieselbe Ausnahme von verschiedenen Stellen in der Implementierung auslöst. Um übermäßigen Code zu vermeiden, erstellen Sie eine Hilfsmethode, die die Ausnahme erstellt und zurückgibt. 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, die die Ausnahme zuordnen und auslösen. Sie sollten diese Methoden aufrufen, anstatt den entsprechenden Ausnahmetyp zu erstellen und zu auslö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, CA1512 und CA1513.

Wenn Sie eine asynchrone Methode implementieren, rufen Sie CancellationToken.ThrowIfCancellationRequested() auf, anstatt zu überprüfen, ob eine Abbruchanforderung vorliegt, und dann ein OperationCanceledException zu erstellen und auszulösen. Weitere Informationen finden Sie unter CA2250.

Einschließen einer lokalisierten Zeichenfolgenmeldung

Die Fehlermeldung, die der Benutzer sieht, wird von der Exception.Message Eigenschaft der ausgelösten Ausnahme und nicht vom Namen der Ausnahmeklasse abgeleitet. In der Regel weisen Sie der Exception.Message Eigenschaft einen Wert zu, indem Sie die Nachrichtenzeichenfolge an das message Argument eines Ausnahmekonstruktors übergeben.

Für lokalisierte Anwendungen sollten Sie eine lokalisierte Nachrichtenzeichenfolge für jede Ausnahme bereitstellen, die ihre Anwendung auslösen kann. Sie verwenden Ressourcendateien, um lokalisierte Fehlermeldungen bereitzustellen. Informationen zum Lokalisieren von Anwendungen und Abrufen lokalisierter Zeichenfolgen finden Sie in den folgenden Artikeln:

Richtige Grammatik verwenden

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 Stapelablaufverfolgung beginnt bei der Anweisung, in der die Ausnahme ausgelöst wird, und endet mit der catch Anweisung, die die Ausnahme erfasst.

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, statische Konstruktoren und Gleichheitsoperatoren, sollten keine Ausnahmen auslösen. Weitere Informationen finden Sie in der Codeanalyseregel CA1065.

Synchrones Auslösen von Argumentüberprüfungs-Ausnahmen

In Aufgabenrückgabemethoden sollten Sie Argumente überprüfen und passende Ausnahmen werfen, zum Beispiel ArgumentException und ArgumentNullException, bevor Sie den asynchronen Teil der Methode eingeben. 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:

Beenden Sie Ausnahmeklassennamen mit Exception

Wenn eine benutzerdefinierte Ausnahme erforderlich ist, benennen Sie sie entsprechend, und leiten Sie sie von der Exception Klasse ab. 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 allgemeinen Konstruktoren: den parameterlosen Konstruktor, einen Konstruktor, der eine Zeichenfolgenmeldung akzeptiert, und einen Konstruktor, der eine Zeichenfolgenmeldung und eine innere Ausnahme akzeptiert.

Ein Beispiel finden Sie in How to: Benutzerdefinierte Ausnahmen erstellen.

Bereitstellen zusätzlicher Eigenschaften nach Bedarf

Stellen Sie zusätzliche Eigenschaften für eine Ausnahme (zusätzlich zur benutzerdefinierten Nachrichtenzeichenfolge) nur bereit, wenn ein programmgesteuertes Szenario vorhanden ist, in dem die zusätzlichen Informationen nützlich sind. Beispielsweise bietet FileNotFoundException die Eigenschaft FileName bereit.

Siehe auch