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
- Obsługa typowych warunków, aby uniknąć wyjątków
- Przechwytywanie wyjątków anulowania i asynchronicznych
- Projektowanie klas, dzięki czemu można uniknąć wyjątków
- Przywróć stan, gdy metody nie są kompletne z powodu wyjątków
- Prawidłowo przechwytuj i ponownie wzamij wyjątki
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 OperationCanceledException
metody , 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żyjthrow
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
- Korzystanie z metod konstruktora wyjątków
- Dołączanie zlokalizowanego komunikatu ciągu
- Użyj prawidłowej gramatyki
- Umieść dobrze instrukcje throw
- Nie zgłaszaj wyjątków w klauzulach finally
- Nie zgłaszaj wyjątków z nieoczekiwanych miejsc
- Synchronicznie zgłaszaj wyjątki weryfikacji argumentó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:
- 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 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:
- Instrukcje: tworzenie wyjątków zdefiniowanych przez użytkownika z zlokalizowanymi komunikatami wyjątków
- Zasoby w aplikacjach platformy .NET
- System.Resources.ResourceManager
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
, GetHashCode
i, 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
- 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 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.
- Exception(), który używa wartości domyślnych.
- Exception(String), który akceptuje komunikat ciągu.
- Exception(String, Exception), który akceptuje 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ść .