Лучшие методики обработки исключений
Хорошо спроектированное приложение обрабатывает исключения и ошибки, чтобы предотвратить сбои приложения. В этой статье описаны рекомендации по обработке и созданию исключений.
Использование блоков 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)
{
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 (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
Выбор конкретного способа зависит от того, насколько часто ожидается возникновение данного события.
Используйте обработку исключений, если событие не происходит часто, то есть если событие действительно исключительное и указывает на ошибку, например непредвиденный конец файла. При использовании обработки исключений в обычных условиях выполняется меньше кода.
Если событие происходит регулярно в рамках нормальной работы программы, выполняйте проверку на наличие ошибок прямо в коде. Проверка на наличие распространенных условий ошибки позволяет выполнять меньший объем кода благодаря устранению исключений.
Устранение исключений при разработке классов
Класс может предоставлять методы и свойства, позволяющие избежать вызова, способного выдать исключение. Например, класс FileStream содержит методы, позволяющие определить, достигнут ли конец файла. Эти методы можно использовать для предотвращения исключения, которое возникает при чтении в конце файла. В следующем примере показано, как считывать конец файла без активации исключения:
class FileRead
{
public:
void ReadAll(FileStream^ fileToRead)
{
// This if statement is optional
// as it is very unlikely that
// the stream would ever be null.
if (fileToRead == nullptr)
{
throw gcnew System::ArgumentNullException();
}
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 void ReadAll(FileStream fileToRead)
{
// This if statement is optional
// as it is very unlikely that
// the stream would ever be null.
if (fileToRead == null)
{
throw new ArgumentNullException();
}
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>
или по умолчанию в качестве индикатора ошибки, что следует учитывать для приложения. При использовании Nullable<Guid>
default
принимает значение null
, а не Guid.Empty
. Иногда добавление Nullable<T>
может сделать его более понятным, если значение присутствует или отсутствует. В других случаях добавление Nullable<T>
может создавать дополнительные случаи для проверка, которые не нужны и служат только для создания потенциальных источников ошибок.
Выдача исключений вместо возврата кода ошибки
Исключения гарантируют, что сбои не замечены, так как вызывающий код не проверка возвращаемого кода.
Использование предопределенных типов исключений .NET
Создавайте новый класс исключений, только если предопределенное исключение не подходит. Например:
- Если вызов набора свойств или метода не подходит, учитывая текущее состояние объекта, создайте InvalidOperationException исключение.
- Если передаются недопустимые параметры, создайте ArgumentException исключение или один из предопределенных классов, производных от ArgumentException.
Завершайте имена классов исключений словом Exception
Если требуется пользовательское исключение, присвойте ему соответствующее имя и сделайте его производным от класса Exception. Например:
public ref class MyFileNotFoundException : public Exception
{
};
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
Включение трех конструкторов в пользовательские классы исключений
При создании собственных классов исключений можно использовать по меньшей мере три общих конструктора: конструктор без параметров, конструктор, принимающий строковое сообщение, и конструктор, принимающий строковое сообщение и внутреннее исключение.
- Exception(), использующий значения по умолчанию.
- Exception(String), принимающий строковое сообщение.
- Exception(String, Exception), принимающий строковое сообщение и внутреннее исключение.
Пример см. в разделе Практическое руководство. Создание пользовательских исключений.
Обеспечение доступности данных об исключении при удаленном выполнении кода
При создании определяемых пользователем исключений убедитесь, что метаданные исключений доступны для кода, выполняющегося удаленно.
Например, в реализациях .NET, поддерживающих домены приложений, исключения могут возникать в доменах приложений. Предположим, домен приложения A создает домен приложения B, который выполняет код, который создает исключение. Чтобы домен приложения A правильно перехватывал и обрабатывал исключение, он должен иметь возможность найти сборку, содержащую исключение, созданное доменом приложения B. Если домен приложения B создает исключение, содержащееся в сборке под его базой приложения, но не в базе приложения A домена приложения, домен приложения A не сможет найти исключение, а среда CLR вызовет FileNotFoundException исключение. Чтобы избежать этой ситуации, можно развернуть сборку, содержащую сведения об исключении двумя способами:
- Поместите эту сборку в общую базу приложения, совместно используемую обоими доменами приложений.
- Если домены не используют общую базу приложений, подпишите сборку, содержащую сведения об исключении с строгим именем и разверните сборку в глобальном кэше сборок.
Использование грамматически правильных сообщений об ошибке
Составляйте понятные предложения, указывая в конце знаки препинания. Каждое предложение в строке, назначенной свойству Exception.Message, должно заканчиваться точкой. Например, "Таблица журнала переполнена".
Включение локализованной строки сообщения в каждое исключение
Сообщение об ошибке, которое видит пользователь, является производным от Exception.Message свойства создаваемого исключения, а не от имени класса исключений. Как правило, значение присваивается Exception.Message свойству путем передачи строки message
сообщения в аргумент конструктора исключений.
Для локализованных приложений необходимо предоставить строку локализованного сообщения для всех исключений, которые может создавать приложение. Используйте файлы ресурсов для предоставления локализованных сообщений об ошибках. Сведения о локализации приложений и извлечении локализованных строк см. в следующих статьях:
- Практическое руководство. Создание определяемых пользователем исключений с локализованными сообщениями об исключениях
- Ресурсы в приложениях .NET
- System.Resources.ResourceManager
Предоставление дополнительных свойств в пользовательских исключениях по мере необходимости
Дополнительные сведения (кроме строки настраиваемого сообщения) включайте в исключение только в случаях, когда в соответствии со сценарием программирования такие дополнительные сведения могут оказаться полезными. Например, исключение FileNotFoundException предоставляет свойство FileName.
Размещение операторов throw для удобной трассировки стека
Трассировка стека начинается в операторе, породившем исключение, и завершается оператором catch
, перехватывающим это исключение.
Использование методов построителя исключений
Обычно класс создает одно и то же исключение из разных мест в его реализации. Чтобы избежать повторения кода, используйте вспомогательные методы, создающие исключение и затем возвращающие его. Например:
ref class FileReader
{
private:
String^ fileName;
public:
FileReader(String^ path)
{
fileName = path;
}
array<Byte>^ Read(int bytes)
{
array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
if (results == nullptr)
{
throw NewFileIOException();
}
return results;
}
FileReaderException^ NewFileIOException()
{
String^ description = "My NewFileIOException Description";
return gcnew FileReaderException(description);
}
};
class FileReader
{
private string fileName;
public FileReader(string path)
{
fileName = path;
}
public byte[] Read(int bytes)
{
byte[] results = FileUtils.ReadFromFile(fileName, bytes);
if (results == null)
{
throw NewFileIOException();
}
return results;
}
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
В некоторых случаях для создания исключения лучше воспользоваться конструктором исключений. В качестве примера можно привести класс глобальных исключений, например ArgumentException.
Восстановление состояния, если методы не выполняются из-за исключения
Вызывающие объекты должны предполагать, что при создании исключения из метода не возникают побочные эффекты. Например, если у вас есть код, который передает деньги, списывая их с одного счета и внося на другой, и при начислении средств возникает исключение, списание средств применяться не должно.
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
Запись исключений для повторного восстановления позже
Чтобы записать исключение и сохранить его вызовы, чтобы иметь возможность повторно выполнить его повторно, используйте System.Runtime.ExceptionServices.ExceptionDispatchInfo класс. Этот класс предоставляет следующие методы и свойства (среди прочего):
- Используется 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
См. также
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по