Udostępnij za pośrednictwem


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żywaj bloków try/catch/finally, aby poradzić sobie z błędami lub zwolnić zasoby

W przypadku kodu, który może potencjalnie wygenerować wyjątek, a gdy aplikacja może się z tego wyjątku odzyskać, stosuj bloki try/catch wokół kodu. W blokach catch zawsze porządkuj wyjątki od najbardziej pochodnych do najmniej pochodnych. (Wszystkie wyjątki pochodzą z klasy Exception. 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 wyżej w stosie wywołań, aby umożliwić odzyskiwanie, jeśli to możliwe.

Wyczyść zasoby, które zostały przydzielone za pomocą instrukcji using lub bloków finally. Preferuj instrukcje using, aby automatycznie czyścić zasoby po wystąpieniu wyjątków. Użyj bloków finally, aby wyczyścić zasoby, które nie implementują IDisposable. Kod w klauzuli finally jest prawie zawsze wykonywany nawet wtedy, gdy są zgłaszane wyjątki.

Zarządzanie powszechnymi warunkami, 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 InvalidOperationException. Można tego uniknąć, używając instrukcji if, aby sprawdzić stan połączenia przed próbą 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 jego zamknięciem, możesz napotkać wyjątek InvalidOperationException.

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. W przypadku korzystania z obsługi wyjątków mniej kodu jest wykonywanych 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.

    Notatka

    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 metod Try* w celu uniknięcia wyjątków

Jeśli koszt wydajności związany z wyjątkami jest zbyt uciążliwy, niektóre metody biblioteki .NET zapewniają alternatywne formy obsługi błędów. Na przykład Int32.Parse zgłasza OverflowException, jeśli wartość, która ma zostać przeanalizowana, jest zbyt duża, aby została reprezentowana przez Int32. Jednak Int32.TryParse nie rzuca tego wyjątku. Zamiast tego zwraca wartość logiczną, a parametr out zawiera przeanalizowaną prawidłową liczbę całkowitą, jeśli operacja się powiedzie. Dictionary<TKey,TValue>.TryGetValue ma podobne zachowanie podczas próby pobrania wartości ze słownika.

Obsługa wyjątków anulowania i asynchronicznych

Lepiej jest przechwytywać OperationCanceledException zamiast TaskCanceledException, który pochodzi z OperationCanceledException, podczas wywoływania metody asynchronicznej. Wiele metod asynchronicznych zgłasza wyjątek OperationCanceledException, jeśli zażądano anulowania. Te wyjątki pozwalają na efektywne zatrzymanie wykonywania, a stos wywołań może być rozwinię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, kiedy na zadanie się oczekuje. 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 klasa FileStream udostępnia metody, które pomagają określić, czy osiągnięto koniec pliku. Możesz wywołać te metody, aby uniknąć wyjątku, który zostanie zgłoszony, jeśli przekroczysz koniec 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 domyślne) w przypadku najczęstszych przypadków błędów zamiast zgłaszania wyjątku. Typowy przypadek błędu można uznać za normalny przepływ sterowania. Zwracając null (lub domyślne) w takich przypadkach, można zminimalizować wpływ na wydajność aplikacji.

W przypadku typów wartości rozważ użycie Nullable<T> lub default jako wskaźnika błędu dla aplikacji. Przy użyciu Nullable<Guid>default staje się null zamiast Guid.Empty. Czasami dodanie Nullable<T> może ułatwić zrozumienie, czy wartość jest obecna czy nieobecna. Innym razem dodanie Nullable<T> może tworzyć dodatkowe przypadki, aby sprawdzić, czy nie są 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

Programy wywołujące powinny być w stanie założyć, że nie ma żadnych skutków ubocznych, gdy wyjątek zostanie zgłoszony z metody. 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ę depozytową 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 throw do ponownego zgłoszenia wyjątku, co ułatwia wywołującym dostrzeżenie rzeczywistej przyczyny problemu bez potrzeby sprawdzania właściwości InnerException. 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órą zawiera, to ślad stosu. Ślad stosu to lista hierarchii wywołań metod rozpoczynająca się od metody, która zgłasza wyjątek i kończy się metodą, która przechwytuje wyjątek. W przypadku ponownego rzucenia wyjątku przez podanie wyjątku w instrukcji throw, na przykład throw e, ślad stosu zostanie ponownie uruchomiony w bieżącej metodzie, a lista wywołań metod między oryginalną metodą, która rzuciła wyjątek, a 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 rzucisz wyjątek z obsługującego go bloku (catch), użyj instrukcji throw 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 rzucasz wyjątek z miejsca innego niż program obsługi (blokcatch), użyj ExceptionDispatchInfo.Capture(Exception), aby przechwycić wyjątek w programie obsługi i ExceptionDispatchInfo.Throw(), gdy chcesz go ponownie rzucić. Możesz użyć właściwości ExceptionDispatchInfo.SourceException, aby sprawdzić przechwycony wyjątek.

W poniższym przykładzie pokazano, jak można użyć klasy ExceptionDispatchInfo 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ć wyjątek InvalidOperationException.
  • W przypadku przekazania nieprawidłowych parametrów należy zgłosić wyjątek ArgumentException lub jedną ze wstępnie zdefiniowanych klas, które pochodzą z ArgumentException.

Notatka

Jeśli to możliwe, najlepiej używać wstępnie zdefiniowanych typów wyjątków, nie należy zgłaszać niektórych zarezerwowanych typów wyjątków, takich jak AccessViolationException, IndexOutOfRangeException, NullReferenceException 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

Często zdarza się, że klasa zgłasza ten sam wyjątek w różnych miejscach swojej 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 metody pomocnicze throw, które przydzielają i zgłaszają wyjątek. Powinno się wywoływać te metody zamiast konstruować i zgłaszać odpowiedni typ wyjątku.

Napiwek

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

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

Dołącz zlokalizowany komunikat tekstowy

Komunikat o błędzie wyświetlany przez użytkownika pochodzi z właściwości Exception.Message wyjątku, który został zgłoszony, a nie z nazwy klasy wyjątku. Zazwyczaj przypisujesz wartość do właściwości Exception.Message, przekazując ciąg komunikatu do argumentu message 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

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

Dobrze rozmieszczaj instrukcje throw

Umieść instrukcje throw tam, gdzie śledzenie stosu będzie pomocne. Odczyt stosu rozpoczyna się od instrukcji, w której następuje zgłoszenie wyjątku i kończy się na instrukcji catch, która przechwytuje wyjątek.

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

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

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

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

Synchronicznie zgłaszaj wyjątki weryfikacji argumentów

W metodach zwracania zadań należy zweryfikować argumenty i zgłosić 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 zwracania zadań.

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 klasy Exception. 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.

Aby zapoznać się z przykładem, zobacz How to: Create user-defined exceptions (Jak utworzyć wyjątki zdefiniowane 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 FileNotFoundException udostępnia właściwość FileName.

Zobacz też