Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Правильная обработка исключений необходима для надежности приложений. Вы можете намеренно обрабатывать ожидаемые исключения, чтобы предотвратить сбой приложения. Однако аварийное приложение является более надежным и диагностическим, чем приложение с неопределенным поведением.
В этой статье описаны рекомендации по обработке и созданию исключений.
Обработка исключений
Ниже приведены рекомендации по обработке исключений.
- Используйте блоки 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 предлагают альтернативные формы обработки ошибок. Например, Int32.Parse выбрасывает OverflowException, если значение, которое необходимо проанализировать, слишком велико, чтобы быть представленным 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
Создание исключений
Ниже приведены лучшие практики по выбрасыванию исключений.
- Использовать стандартные типы исключений
- Использование методов построителя исключений
- Включить локализованное строковое сообщение
- Использовать правильную грамматику
- Разместите операторы throw правильно
- не вызывайте исключения в предложениях, наконец,
- Не вызывайте исключения из неожиданных мест
- выбрасывать исключения проверки аргументов синхронно
Использование предопределенных типов исключений
Введите новый класс исключений только в том случае, если предопределенный класс не применяется. Например:
- Если вызов набора свойств или метода не соответствует текущему состоянию объекта, создайте исключение InvalidOperationException.
- Если передаются недопустимые параметры, вызовите исключение ArgumentException или один из предопределенных классов, производных от ArgumentException.
Заметка
Хотя по возможности рекомендуется использовать предопределенные типы исключений, вы не должны вызывать некоторые зарезервированные типы исключений, такие как ,, AccessViolationException, IndexOutOfRangeException, NullReferenceException и 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-блоках
Не вызывайте исключения в предложениях 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.
См. также
- Рекомендации по проектированию исключений