Best Practices für Ausnahmen

Eine ausgereifte App behandelt Ausnahmen und Fehler, um App-Abstürze zu verhindern. In diesem Abschnitt 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.

Verwenden Sie try/catch Blöcke um Code, die möglicherweise eine Ausnahme generieren können, und Ihr Code kann von dieser Ausnahme wiederhergestellt werden. Ordnen Sie Ausnahmen in catch-Blöcken immer von der am stärksten abgeleiteten zur am wenigsten abgeleiteten. Alle Ausnahmen werden von der Exception Klasse abgeleitet. Weitere abgeleitete Ausnahmen werden nicht durch eine Catch-Klausel behandelt, die einer Catch-Klausel für eine Basis ausnahmeklasse vorausgeht. Wenn Der Code von 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 Ressourcen, die entweder using Anweisungen oder finally Blöcken zugeordnet sind. 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 beispielsweise versuchen, eine bereits geschlossene Verbindung 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 Verbindungszustand 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 (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 häufig auftritt, das heißt, 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 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. Diese Methoden können verwendet werden, um die Ausnahme zu vermeiden, die ausgelöst wird, wenn Sie das Ende der Datei lesen. Im folgenden Beispiel wird gezeigt, wie Sie das Ende einer Datei lesen, ohne eine Ausnahme auszulösen:

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, Null (oder Standard) für die häufigsten Fehlerfälle zurückzugeben, anstatt eine Ausnahme zu auslösen. Ein gängiger Fehlerfall kann als normaler Kontrollfluss betrachtet werden. Indem Sie in diesen Fällen NULL (oder einen Standardwert) zurückgeben, minimieren Sie die Auswirkungen auf die Leistung einer App.

Für Werttypen, ob sie als Fehleranzeige verwendet oder standardmäßig verwendet Nullable<T> werden sollen, ist etwas, das für Ihre App berücksichtigt werden soll. Durch Verwendung von Nullable<Guid> wird default zu null statt zu Guid.Empty. Manchmal kann das Hinzufügen Nullable<T> deutlicher sein, wenn ein Wert vorhanden oder nicht vorhanden ist. In anderen Zeiten kann das Hinzufügen Nullable<T> zusätzliche Fälle erstellen, um zu überprüfen, dass dies nicht erforderlich ist und nur zum Erstellen potenzieller Fehlerquellen dient.

Auslösen von Ausnahmen statt Zurückgeben eines Fehlercodes

Ausnahmen stellen sicher, dass Fehler nicht nicht beachtet werden, da der Aufrufcode keinen Rückgabecode überprüft hat.

Verwenden der vordefinierten .NET-Ausnahmetypen

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

  • Löst eine Ausnahme aus, wenn ein InvalidOperationException Eigenschaftssatz oder ein Methodenaufruf aufgrund des aktuellen Zustands des Objekts nicht geeignet ist.

  • Lösen Sie eine ArgumentException oder eine der von ArgumentException abgeleiteten vordefinierten Klassen in dem Fall aus, dass ungültige Parameter übergeben werden.

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

Wenn Sie benutzerdefinierte Ausnahmen erstellen, stellen Sie sicher, dass die Metadaten für die Ausnahmen für Code verfügbar sind, die remote ausgeführt werden.

In .NET-Implementierungen, die App-Domänen unterstützen, können Ausnahmen beispielsweise in App-Domänen 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äß abfangen und behandeln kann, muss die Assembly gefunden werden, die die Ausnahme enthält, die von App Domain B ausgelöst wird. Wenn App-Domäne B eine Ausnahme auslöst, die in einer Assembly unter der Anwendungsbasis enthalten ist, aber nicht unter der Anwendungsbasis von App Domain A, kann App Domain A die Ausnahme nicht finden, und die allgemeine Sprachlaufzeit löst eine FileNotFoundException Ausnahme aus. Um diese Situation zu vermeiden, können Sie die Assembly bereitstellen, die die Ausnahmeinformationen auf zwei Arten enthält:

  • Sie legen die Assembly in einer gemeinsamen Anwendungsbasis ab, die von beiden App-Domänen verwendet wird.

  • Wenn die Domänen keine gemeinsame Anwendungsbasis freigeben, signieren Sie die Assembly, die die Ausnahmeinformationen mit einem starken Namen enthält, und stellen Sie die Assembly im globalen Assemblycache bereit.

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. Beispielsweise wäre "Die Protokolltabelle überlauft.", wäre eine entsprechende Nachrichtenzeichenfolge.

Einschließen einer lokalisierten Meldungszeichenfolge in jede Ausnahme

Die Fehlermeldung, die der Benutzer sieht, wird aus der Eigenschaft der Ausnahme abgeleitet, die ausgelöst wurde, und nicht vom Exception.Message Namen der Ausnahmeklasse. Normalerweise weisen Sie der Exception.Message Eigenschaft einen Wert zu, indem Sie die Nachrichtenzeichenfolge 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

Es ist üblich, dass eine Klasse dieselbe Ausnahme von verschiedenen Stellen in der Implementierung auslöst. 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 Ausnahmen direkt aus. Sie müssen jedoch die Methode schreiben, sodass die Auszahlung umgekehrt 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

In diesem Beispiel wird die Verwendung throw der ursprünglichen Ausnahme veranschaulicht, wodurch es anrufern einfacher wird, die tatsächliche Ursache des Problems zu sehen, ohne die Eigenschaft zu untersuchen InnerException . Eine Alternative besteht darin, eine neue Ausnahme zu auslö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

Siehe auch