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 bloków try/catch/finally, aby odzyskać po błędach lub zwolnić zasoby
- Zarządzaj typowymi warunkami, aby uniknąć wyjątków
- Przechwytywanie wyjątków anulowania i asynchronicznych
- klasy projektowania, dzięki czemu można uniknąć wyjątków
- Stan przywracania, gdy metody nie są ukończone z powodu wyjątków
- Przechwytuj i ponownie rzucaj wyjątki prawidłowo
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 instrukcjithrow
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 (blok
catch
), 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żyj wstępnie zdefiniowanych typów wyjątków
- Używanie metod konstruktora wyjątków
- zawierać zlokalizowany komunikat ciągu znaków
- Użyj prawidłowej gramatyki
- Odpowiednio umieść instrukcje throw
- Nie wywołuj wyjątków w klauzulach finally
- nie zgłaszaj wyjątków z nieoczekiwanych miejsc
- Zgłaszaj wyjątki weryfikacji argumentów synchronicznie
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.
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty(String, String)
- ArgumentException.ThrowIfNullOrWhiteSpace(String, String)
- ArgumentOutOfRangeException.ThrowIfZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfNegative<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, String)
- ObjectDisposedException.ThrowIf
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:
- Jak: tworzyć wyjątki zdefiniowane przez użytkownika z lokalizowanymi komunikatami wyjątków
- zasoby w aplikacjach platformy .NET
- System.Resources.ResourceManager
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
, GetHashCode
i 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
- uwzględnij trzy konstruktory
- Podaj dodatkowe właściwości zgodnie z potrzebami
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.
- Exception(), która używa wartości domyślnych.
- Exception(String), który akceptuje komunikat tekstowy.
- Exception(String, Exception), który akceptuje komunikat jako ciąg znaków 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.