Najlepsze rozwiązania dotyczące wyjątków

Właściwa obsługa wyjątków jest niezbędna w przypadku niezawodności aplikacji. Można celowo obsługiwać oczekiwane wyjątki, aby zapobiec awarii aplikacji. Jednak awaria aplikacji jest bardziej niezawodna i rozpoznawalna niż aplikacja z niezdefiniowanym zachowaniem.

W tym artykule opisano najlepsze rozwiązania dotyczące obsługi i tworzenia wyjątków.

Obsługa wyjątków

Poniższe najlepsze rozwiązania dotyczą sposobu obsługi wyjątków:

Użyj polecenia try/catch/finally, aby odzyskać odzyskiwanie po błędach lub wydać zasoby

W przypadku kodu, który może potencjalnie wygenerować wyjątek, a gdy aplikacja może odzyskać dane z tego wyjątku, użyj try/catch bloków wokół kodu. W catch blokach zawsze porządkuj wyjątki od najbardziej pochodnych do najmniej pochodnych. (Wszystkie wyjątki pochodzą z Exception klasy . Więcej wyjątków pochodnych nie jest obsługiwanych przez klauzulę catch poprzedzoną klauzulą catch dla klasy wyjątków podstawowych). Jeśli kod nie może odzyskać sprawności po wystąpieniu wyjątku, nie przechwyć tego wyjątku. Włącz metody w górę stosu wywołań, aby odzyskać, jeśli to możliwe.

Czyszczenie zasobów przydzielonych za pomocą using instrukcji lub finally bloków. Preferuj using instrukcje automatycznego czyszczenia zasobów, gdy są zgłaszane wyjątki. Użyj finally bloków, aby wyczyścić zasoby, które nie implementują IDisposableprogramu . Kod w klauzuli finally jest prawie zawsze wykonywany nawet wtedy, gdy wyjątki są zgłaszane.

Obsługa typowych warunków, aby uniknąć wyjątków

W przypadku warunków, które prawdopodobnie wystąpią, ale mogą wywołać wyjątek, rozważ ich obsługę w sposób, który pozwala uniknąć wyjątku. Jeśli na przykład spróbujesz zamknąć połączenie, które zostało już zamknięte, otrzymasz element InvalidOperationException. Można tego uniknąć, używając instrukcji , aby sprawdzić stan połączenia przed próbą if jego zamknięcia.

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

Jeśli nie sprawdzisz stanu połączenia przed zamknięciem, możesz przechwycić InvalidOperationException wyjątek.

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

Podejście do wyboru zależy od tego, jak często spodziewasz się wystąpienia zdarzenia.

  • Użyj obsługi wyjątków, jeśli zdarzenie nie występuje często, oznacza to, że jeśli zdarzenie jest naprawdę wyjątkowe i wskazuje błąd, taki jak nieoczekiwany koniec pliku. Użycie obsługi wyjątków powoduje, że mniej kodu jest wykonywanego w normalnych warunkach.

  • Sprawdź warunki błędu w kodzie, jeśli zdarzenie występuje rutynowo i można je uznać za część normalnego wykonywania. Podczas sprawdzania typowych warunków błędów jest wykonywany mniej kodu, ponieważ unikasz wyjątków.

    Uwaga

    Kontrole z góry eliminują wyjątki przez większość czasu. Mogą jednak wystąpić warunki wyścigu, w których chroniony stan zmienia się między sprawdzaniem a operacją, a w takim przypadku nadal może wystąpić wyjątek.

Wywoływanie Try* metod w celu uniknięcia wyjątków

Jeśli koszt wydajności wyjątków jest zbyt uciążliwy, niektóre metody biblioteki platformy .NET zapewniają alternatywne formy obsługi błędów. Na przykład zwraca wartość OverflowException , jeśli wartość do przeanalizowana jest zbyt duża, Int32.Parse aby być reprezentowana przez Int32element . Int32.TryParse Nie zgłasza jednak tego wyjątku. Zamiast tego zwraca wartość logiczną i ma parametr zawierający przeanalizowaną prawidłową out liczbę całkowitą po powodzeniu. Dictionary<TKey,TValue>.TryGetValue Ma podobne zachowanie podczas próby pobrania wartości ze słownika.

Przechwytywanie wyjątków anulowania i asynchronicznych

Lepiej jest przechwycić OperationCanceledException metodę zamiast TaskCanceledException, która pochodzi z OperationCanceledExceptionmetody , podczas wywoływania metody asynchronicznej. Wiele metod asynchronicznych zgłasza wyjątek, OperationCanceledException jeśli zażądano anulowania. Te wyjątki umożliwiają efektywne zatrzymanie wykonywania, a stos wywołań zostanie odsunięty po zaobserwowaniu żądania anulowania.

Metody asynchroniczne przechowują wyjątki zgłaszane podczas wykonywania w zwracanym zadaniu. Jeśli wyjątek jest przechowywany w zwróconym zadaniu, ten wyjątek zostanie zgłoszony, gdy zadanie będzie oczekiwać. Wyjątki użycia, takie jak ArgumentException, są nadal zgłaszane synchronicznie. Aby uzyskać więcej informacji, zobacz Wyjątki asynchroniczne.

Projektowanie klas, dzięki czemu można uniknąć wyjątków

Klasa może udostępniać metody lub właściwości, które umożliwiają uniknięcie wywołania, które wyzwoliłoby wyjątek. Na przykład FileStream klasa udostępnia metody, które pomagają określić, czy osiągnięto koniec pliku. Te metody można wywołać, aby uniknąć wyjątku, który zostanie zgłoszony, jeśli przeczytasz obok końca pliku. W poniższym przykładzie pokazano, jak odczytać na końcu pliku bez wyzwalania wyjątku:

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

Innym sposobem uniknięcia wyjątków jest zwrócenie null (lub ustawienie domyślne) dla najczęściej występujących przypadków błędów zamiast zgłaszania wyjątku. Typowy przypadek błędu można uznać za normalny przepływ sterowania. null Zwracając (lub domyślnie) w takich przypadkach, można zminimalizować wpływ wydajności na aplikację.

W przypadku typów wartości rozważ Nullable<T> użycie lub default jako wskaźnik błędu dla aplikacji. Za pomocą polecenia Nullable<Guid>default program staje null się zamiast Guid.Empty. Czasami dodanie Nullable<T> może wyjaśnić, kiedy wartość jest obecna lub nieobecna. Innym razem dodanie Nullable<T> może tworzyć dodatkowe przypadki w celu sprawdzenia, czy nie jest to konieczne, i służy tylko do tworzenia potencjalnych źródeł błędów.

Przywróć stan, gdy metody nie są kompletne z powodu wyjątków

Obiekty wywołujące powinny być w stanie założyć, że nie występują efekty uboczne, gdy wyjątek jest zgłaszany przez metodę. Jeśli na przykład masz kod, który przelewa pieniądze poprzez wycofanie z jednego konta i depozyt na innym koncie, a wyjątek jest zgłaszany podczas wykonywania depozytu, nie chcesz, aby wypłata pozostała w mocy.

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

Poprzednia metoda nie zgłasza bezpośrednio żadnych wyjątków. Należy jednak napisać metodę, aby wypłata została odwrócona, jeśli operacja depozytu zakończy się niepowodzeniem.

Jednym ze sposobów radzenia sobie z tą sytuacją jest przechwycenie wszelkich wyjątków zgłoszonych przez transakcję depozytów i wycofanie wypłaty.

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

W tym przykładzie pokazano użycie metody throw w celu ponownego wywołania wyjątku, co ułatwia obiektom wywołującym sprawdzenie rzeczywistej przyczyny problemu bez konieczności badania InnerException właściwości. Alternatywą jest zgłoszenie nowego wyjątku i dołączenie oryginalnego wyjątku jako wyjątku wewnętrznego.

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

Prawidłowo przechwytuj i ponownie wzamij wyjątki

Po wyrzuceniu wyjątku część informacji, które prowadzi, to ślad stosu. Ślad stosu to lista hierarchii wywołań metody rozpoczynająca się od metody, która zgłasza wyjątek i kończy się metodą, która przechwytuje wyjątek. W przypadku ponownego wywołania wyjątku przez określenie wyjątku w throw instrukcji, na przykład throw e, ślad stosu zostanie uruchomiony ponownie w bieżącej metodzie i lista wywołań metod między oryginalną metodą, która zwróciła wyjątek i bieżącą metodę zostanie utracona. Aby zachować oryginalne informacje o śledzeniu stosu z wyjątkiem, istnieją dwie opcje, które zależą od tego, gdzie następuje ponowne wywołanie wyjątku:

  • Jeśli ponownie wywrócisz wyjątek z poziomu programu obsługi (catch bloku) przechwyconego wystąpienia wyjątku, użyj throw instrukcji bez określania wyjątku. Reguła analizy kodu CA2200 ułatwia znajdowanie miejsc w kodzie, w których można przypadkowo utracić informacje śledzenia stosu.
  • Jeśli ponownie wywrócisz wyjątek z innego miejsca niż program obsługi (catch blok), użyj polecenia ExceptionDispatchInfo.Capture(Exception) , aby przechwycić wyjątek w procedurze obsługi i ExceptionDispatchInfo.Throw() kiedy chcesz go ponownie wywrócić. Za pomocą ExceptionDispatchInfo.SourceException właściwości można sprawdzić przechwycony wyjątek.

W poniższym przykładzie pokazano, jak ExceptionDispatchInfo można użyć klasy i jak mogą wyglądać dane wyjściowe.

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();

Jeśli plik w przykładowym kodzie nie istnieje, generowane są następujące dane wyjściowe:

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

Zgłaszanie wyjątków

Poniższe najlepsze rozwiązania dotyczą sposobu zgłaszania wyjątków:

Używanie wstępnie zdefiniowanych typów wyjątków

Wprowadź nową klasę wyjątków tylko wtedy, gdy wstępnie zdefiniowana klasa nie ma zastosowania. Na przykład:

  • Jeśli zestaw właściwości lub wywołanie metody nie jest odpowiednie, biorąc pod uwagę bieżący stan obiektu, należy zgłosić InvalidOperationException wyjątek.
  • W przypadku przekazania nieprawidłowych parametrów należy zgłosić ArgumentException wyjątek lub jedną ze wstępnie zdefiniowanych klas, które pochodzą z klasy ArgumentException.

Uwaga

Chociaż najlepiej używać wstępnie zdefiniowanych typów wyjątków, jeśli to możliwe, nie należy zgłaszać niektórych zarezerwowanych typów wyjątków, takich jak AccessViolationException, IndexOutOfRangeExceptionNullReferenceException i StackOverflowException. Aby uzyskać więcej informacji, zobacz CA2201: Nie zgłaszaj zarezerwowanych typów wyjątków.

Korzystanie z metod konstruktora wyjątków

Klasa często zgłasza ten sam wyjątek z różnych miejsc w implementacji. Aby uniknąć nadmiernego kodu, utwórz metodę pomocnika, która tworzy wyjątek i zwraca go. Na przykład:

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

Niektóre kluczowe typy wyjątków platformy .NET mają takie statyczne throw metody pomocnicze, które przydzielają i zgłaszają wyjątek. Należy wywołać te metody zamiast konstruować i zgłaszać odpowiedni typ wyjątku:

Napiwek

Następujące reguły analizy kodu mogą pomóc w znalezieniu miejsc w kodzie, w których można korzystać z tych statycznych throw pomocników: CA1510, CA1511, CA1512 i CA1513.

Jeśli implementujesz metodę asynchroniczną, wywołaj metodę CancellationToken.ThrowIfCancellationRequested() zamiast sprawdzania, czy zażądano anulowania, a następnie skonstruuj i zgłaszaj OperationCanceledExceptionpolecenie . Aby uzyskać więcej informacji, zobacz CA2250.

Dołączanie zlokalizowanego komunikatu ciągu

Komunikat o błędzie, który widzi użytkownik, pochodzi z Exception.Message właściwości wyjątku, który został zgłoszony, a nie z nazwy klasy wyjątku. Zazwyczaj przypisujesz wartość do Exception.Message właściwości, przekazując ciąg komunikatu do message argumentu konstruktora wyjątku.

W przypadku zlokalizowanych aplikacji należy podać zlokalizowany ciąg komunikatu dla każdego wyjątku, który może zgłosić aplikacja. Pliki zasobów służą do dostarczania zlokalizowanych komunikatów o błędach. Aby uzyskać informacje na temat lokalizowania aplikacji i pobierania zlokalizowanych ciągów, zobacz następujące artykuły:

Użyj prawidłowej gramatyki

Zapisuj jasne zdania i dołączaj znaki interpunkcyjne kończące. Każde zdanie w ciągu przypisanym do Exception.Message właściwości powinno kończyć się kropką. Na przykład "Tabela dziennika została przepełniona". Używa poprawnej gramatyki i interpunkcji.

Umieść dobrze instrukcje throw

Umieść instrukcje throw, w których pomocne będzie śledzenie stosu. Ślad stosu rozpoczyna się od instrukcji, w której jest zgłaszany wyjątek i kończy się na catch instrukcji, która przechwytuje wyjątek.

Nie zgłaszaj wyjątków w klauzulach finally

Nie zgłaszaj wyjątków w finally klauzulach. Aby uzyskać więcej informacji, zobacz Reguła analizy kodu CA2219.

Nie zgłaszaj wyjątków z nieoczekiwanych miejsc

Niektóre metody, takie jak Equals, GetHashCodei, ToString konstruktory statyczne i operatory równości, nie powinny zgłaszać wyjątków. Aby uzyskać więcej informacji, zobacz Reguła analizy kodu CA1065.

Synchronicznie zgłaszaj wyjątki weryfikacji argumentów

W metodach zwracania zadań należy zweryfikować argumenty i zgłosić wszelkie odpowiednie wyjątki, takie jak ArgumentException i ArgumentNullException, przed wprowadzeniem asynchronicznej części metody. Wyjątki, które są zgłaszane w asynchronicznej części metody, są przechowywane w zwracanym zadaniu i nie pojawiają się, dopóki na przykład zadanie nie zostanie oczekiwane. Aby uzyskać więcej informacji, zobacz Wyjątki w metodach zwracanych przez zadania.

Niestandardowe typy wyjątków

Poniższe najlepsze rozwiązania dotyczą niestandardowych typów wyjątków:

Kończ nazwy klas wyjątków z Exception

Jeśli jest wymagany wyjątek niestandardowy, należy go odpowiednio nazwać i uzyskać od Exception klasy. Na przykład:

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

Uwzględnij trzy konstruktory

Użyj co najmniej trzech typowych konstruktorów podczas tworzenia własnych klas wyjątków: konstruktora bez parametrów, konstruktora, który przyjmuje komunikat ciągu, oraz konstruktora, który przyjmuje komunikat ciągu i wyjątek wewnętrzny.

Przykład można znaleźć w temacie How to: Create user-defined exceptions (Instrukcje: tworzenie wyjątków zdefiniowanych przez użytkownika).

Podaj dodatkowe właściwości zgodnie z potrzebami

Podaj dodatkowe właściwości wyjątku (oprócz niestandardowego ciągu komunikatu) tylko wtedy, gdy istnieje scenariusz programowy, w którym dodatkowe informacje są przydatne. Na przykład element FileNotFoundException udostępnia FileName właściwość .

Zobacz też