Лучшие методики обработки исключений
Правильная обработка исключений необходима для надежности приложений. Вы можете намеренно обрабатывать ожидаемые исключения, чтобы предотвратить сбой приложения. Однако аварийное приложение является более надежным и диагностическим, чем приложение с неопределенным поведением.
В этой статье описаны рекомендации по обработке и созданию исключений.
Обработка исключений
Ниже приведены рекомендации по обработке исключений.
- Использование блоков try/catch/finally для восстановления из ошибок или ресурсов выпуска
- Обработка распространенных условий, чтобы избежать исключений
- Перехват отмены и асинхронных исключений
- Классы конструктора, чтобы избежать исключений
- Восстановление состояния, когда методы не завершают работу из-за исключений
- Правильное запись и повторная обработка исключений
Использование блоков try/catch/finally для восстановления после ошибок или высвобождения ресурсов
Для кода, который потенциально может создать исключение, и когда приложение может восстановиться после этого исключения, используйте try
/catch
блоки вокруг кода. В блоках catch
следует всегда упорядочивать исключения от более производных к менее производным. (Все исключения, производные от Exception класса. Более производные исключения не обрабатываются catch
предложением, catch
предшествующим предложению базового класса исключений.) Если код не может восстановиться из исключения, не перехватывайте это исключение. Включите методы выше по стеку вызовов для восстановления по мере возможности.
Очистка ресурсов, выделенных с помощью using
инструкций или finally
блоков. Рекомендуется использовать инструкции using
для автоматической очистки ресурсов при возникновении исключений. Используйте блоки finally
, чтобы очистить ресурсы, которые не реализуют IDisposable. Код в предложении finally
выполняется почти всегда — даже при возникновении исключений.
Обработка распространенных условий, чтобы избежать исключений
Для условий, которые могут возникнуть, но могут активировать исключение, рассмотрите возможность их обработки таким образом, чтобы избежать исключения. Например, если вы попытаетесь закрыть подключение, которое уже закрыто, вы получите InvalidOperationException
. Этого можно избежать, используя оператор if
для проверки состояния подключения перед попыткой закрыть его.
if (conn.State != ConnectionState.Closed)
{
conn.Close();
}
If conn.State <> ConnectionState.Closed Then
conn.Close()
End IF
Если вы не проверка состояние подключения перед закрытием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
Выбор подхода зависит от частоты возникновения события.
Используйте обработку исключений, если событие не происходит часто, то есть если событие действительно исключительное и указывает на ошибку, например непредвиденный конец файла. При использовании обработки исключений в обычных условиях выполняется меньше кода.
Если событие происходит регулярно в рамках нормальной работы программы, выполняйте проверку на наличие ошибок прямо в коде. Проверка на наличие распространенных условий ошибки позволяет выполнять меньший объем кода благодаря устранению исключений.
Примечание.
Внешние проверка устраняют исключения в большинстве случаев. Однако могут быть условия гонки, в которых защищенные условия изменяются между проверка и операцией, и в этом случае можно по-прежнему вызывать исключение.
Вызов Try*
методов для предотвращения исключений
Если затраты на производительность исключений запрещены, некоторые методы библиотеки .NET предоставляют альтернативные формы обработки ошибок. Например, вызывает OverflowException исключение, если значение, которое нужно проанализировать, Int32.Parse слишком велико, чтобы быть представленоInt32. Int32.TryParse Однако этот исключение не вызывается. Вместо этого он возвращает логическое значение и имеет out
параметр, содержащий допустимое целое число при успешном выполнении. Dictionary<TKey,TValue>.TryGetValue имеет аналогичное поведение для попытки получить значение из словаря.
Перехват отмены и асинхронных исключений
Лучше перехватывать OperationCanceledException вместо TaskCanceledExceptionтого, что является производным от OperationCanceledException
, при вызове асинхронного метода. Многие асинхронные методы вызывают OperationCanceledException исключение, если запрашивается отмена. Эти исключения позволяют эффективно останавливать выполнение и вызывать вызовы после наблюдения запроса на отмену.
Асинхронные методы хранят исключения, которые создаются во время выполнения в возвращаемой задаче. Если исключение хранится в возвращаемой задаче, это исключение будет создано при ожидании задачи. Исключения использования, например ArgumentException, по-прежнему создаются синхронно. Дополнительные сведения см. в статье об асинхронных исключениях.
Устранение исключений при разработке классов
Класс может предоставлять методы и свойства, позволяющие избежать вызова, способного выдать исключение. Например, класс предоставляет методы, FileStream помогающие определить, достигнут ли конец файла. Эти методы можно вызвать, чтобы избежать исключения, которое возникает при чтении в конце файла. В следующем примере показано, как считывать конец файла без активации исключения:
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
Еще одним способом избежать исключений является возврат null
(или по умолчанию) для большинства распространенных случаев ошибок вместо того, чтобы вызвать исключение. Распространенный случай ошибки можно рассматривать как обычный поток управления. Возвращая null
(или по умолчанию) в этих случаях, можно свести к минимуму влияние производительности на приложение.
Для типов значений рекомендуется использовать Nullable<T>
или default
в качестве индикатора ошибки для приложения. При использовании Nullable<Guid>
default
принимает значение null
, а не Guid.Empty
. Иногда добавление Nullable<T>
может сделать его более понятным, если значение присутствует или отсутствует. В других случаях добавление Nullable<T>
может создавать дополнительные случаи для проверка, которые не нужны и служат только для создания потенциальных источников ошибок.
Восстановление состояния, если методы не выполняются из-за исключения
Вызывающие объекты должны предполагать, что при создании исключения из метода не возникают побочные эффекты. Например, если у вас есть код, который передает деньги, списывая их с одного счета и внося на другой, и при начислении средств возникает исключение, списание средств применяться не должно.
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
Предыдущий метод не создает никаких исключений напрямую. Однако необходимо написать метод, чтобы отмена вывода была отменена, если операция депозита завершается сбоем.
Один из способов обработки в этой ситуации заключается в перехвате всех исключений, выданных транзакцией начисления средств, и откате транзакции списания средств.
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
В этом примере показано использование throw
повторного изменения исходного исключения, что упрощает для вызывающих абонентов видеть реальную причину проблемы без необходимости проверки InnerException свойства. Альтернативой является создание нового исключения и включение исходного исключения в качестве внутреннего исключения.
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
Правильное запись и повторная обработка исключений
При возникновении исключения часть информации в нем представляет собой трассировку стека. Трассировка стека — это список иерархии вызовов методов, который начинается с метода, вызывающего исключение, и завершается методом, перехватывающим исключение. При повторном создании исключения путем указания исключения в throw
инструкции, например, throw e
трассировка стека перезапускается в текущем методе, а список вызовов методов между исходным методом, который вызвал исключение, и текущий метод теряется. Чтобы сохранить исходные данные трассировки стека с исключением, существует два варианта, которые зависят от того, откуда выполняется повторение исключения:
- Если вы повторно создаете исключение из обработчика (
catch
блок), который поймал экземпляр исключения, используйтеthrow
инструкцию без указания исключения. Правило анализа кода CA2200 помогает найти места в коде, где могут случайно потерять сведения трассировки стека. - Если вы выполняете повторное создание исключения из другого места, отличного от обработчика (
catch
блок), используйте ExceptionDispatchInfo.Capture(Exception) для записи исключения в обработчике и ExceptionDispatchInfo.Throw() при необходимости повторного его увеличения. Свойство можно использовать ExceptionDispatchInfo.SourceException для проверки захваченного исключения.
В следующем примере показано, как ExceptionDispatchInfo можно использовать класс и как выглядеть выходные данные.
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();
Если файл в примере кода не существует, создается следующий результат:
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
Создание исключений
Ниже приведены рекомендации по устранению исключений.
- Использование предопределенных типов исключений
- Использование методов построителя исключений
- Включение локализованного строкового сообщения
- Использование правильной грамматики
- Хорошо разместить операторы бросить
- Не вызывайте исключения в предложениях, наконец,
- Не вызывайте исключения из непредвиденных мест
- Создание исключений проверки аргументов синхронно
Использование предопределенных типов исключений
Создавайте новый класс исключений, только если предопределенное исключение не подходит. Например:
- Если вызов набора свойств или метода не подходит, учитывая текущее состояние объекта, создайте InvalidOperationException исключение.
- Если передаются недопустимые параметры, создайте ArgumentException исключение или один из предопределенных классов, производных от ArgumentException.
Примечание.
Хотя рекомендуется использовать предопределенные типы исключений, если это возможно, не следует создавать некоторые зарезервированные типы исключений, например AccessViolationException, IndexOutOfRangeExceptionNullReferenceException и StackOverflowException. Дополнительные сведения см. в разделе CA2201: не вызывайте зарезервированные типы исключений.
Использование методов построителя исключений
Обычно класс создает одно и то же исключение из разных мест в его реализации. Чтобы избежать чрезмерного кода, создайте вспомогательный метод, который создает исключение и возвращает его. Например:
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
Некоторые ключевые типы исключений .NET имеют такие статические throw
вспомогательные методы, которые выделяют и вызывают исключение. Эти методы следует вызывать вместо создания и создания соответствующего типа исключения:
- 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
Совет
Следующие правила анализа кода помогут найти места в коде, где можно воспользоваться этими статическими throw
вспомогательными средствами: CA1510, CA1511, CA1512 и CA1513.
Если вы реализуете асинхронный метод, вызовите CancellationToken.ThrowIfCancellationRequested() вместо проверка, если отмена была запрошена, а затем создание и создание и созданиеOperationCanceledException. Дополнительные сведения см. в разделе CA2250.
Включение локализованного строкового сообщения
Сообщение об ошибке, которое видит пользователь, является производным от Exception.Message свойства создаваемого исключения, а не от имени класса исключений. Как правило, значение присваивается Exception.Message свойству путем передачи строки message
сообщения в аргумент конструктора исключений.
Для локализованных приложений необходимо предоставить строку локализованного сообщения для всех исключений, которые может создавать приложение. Используйте файлы ресурсов для предоставления локализованных сообщений об ошибках. Сведения о локализации приложений и извлечении локализованных строк см. в следующих статьях:
- Практическое руководство. Создание определяемых пользователем исключений с локализованными сообщениями об исключениях
- Ресурсы в приложениях .NET
- System.Resources.ResourceManager
Использование правильной грамматики
Составляйте понятные предложения, указывая в конце знаки препинания. Каждое предложение в строке, назначенной свойству Exception.Message, должно заканчиваться точкой. Например, "Таблица журнала переполнена". Использует правильную грамматику и знак препинания.
Хорошо разместить операторы бросить
Поместите инструкции, в которых трассировка стека будет полезной. Трассировка стека начинается в операторе, породившем исключение, и завершается оператором catch
, перехватывающим это исключение.
Не вызывайте исключения в предложениях, наконец,
Не вызывайте исключения в finally
предложениях. Дополнительные сведения см. в правиле анализа кода CA2219.
Не вызывайте исключения из непредвиденных мест
Некоторые методы, такие как Equals
, GetHashCode
и ToString
методы, статические конструкторы и операторы равенства, не должны вызывать исключения. Дополнительные сведения см. в правиле анализа кода CA1065.
Создание исключений проверки аргументов синхронно
В методах возврата задач необходимо проверить аргументы и вызвать все соответствующие исключения, например ArgumentException и ArgumentNullExceptionперед вводом асинхронной части метода. Исключения, создаваемые в асинхронной части метода, хранятся в возвращаемой задаче и не появляются, пока, например, задача ожидается. Дополнительные сведения см. в разделе "Исключения" в методах возврата задач.
Пользовательские типы исключений
Следующие рекомендации касаются пользовательских типов исключений:
- Конечные имена классов исключений с помощью
Exception
- Включение трех конструкторов
- Укажите дополнительные свойства по мере необходимости
Конечные имена классов исключений с помощью Exception
Если требуется пользовательское исключение, присвойте ему соответствующее имя и сделайте его производным от класса Exception. Например:
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Включение трех конструкторов
При создании собственных классов исключений можно использовать по меньшей мере три общих конструктора: конструктор без параметров, конструктор, принимающий строковое сообщение, и конструктор, принимающий строковое сообщение и внутреннее исключение.
- Exception(), использующий значения по умолчанию.
- Exception(String), принимающий строковое сообщение.
- Exception(String, Exception), принимающий строковое сообщение и внутреннее исключение.
Пример см. в статье "Практическое руководство. Создание определяемых пользователем исключений".
Укажите дополнительные свойства по мере необходимости
Дополнительные сведения (кроме строки настраиваемого сообщения) включайте в исключение только в случаях, когда в соответствии со сценарием программирования такие дополнительные сведения могут оказаться полезными. Например, исключение FileNotFoundException предоставляет свойство FileName.