Лучшие методики обработки исключений

Хорошо спроектированное приложение обрабатывает исключения и ошибки, чтобы предотвратить сбои приложения. В этой статье описаны рекомендации по обработке и созданию исключений.

Использование блоков 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 сообщения в аргумент конструктора исключений.

Для локализованных приложений необходимо предоставить строку локализованного сообщения для всех исключений, которые может создавать приложение. Используйте файлы ресурсов для предоставления локализованных сообщений об ошибках. Сведения о локализации приложений и извлечении локализованных строк см. в следующих статьях:

Предоставление дополнительных свойств в пользовательских исключениях по мере необходимости

Дополнительные сведения (кроме строки настраиваемого сообщения) включайте в исключение только в случаях, когда в соответствии со сценарием программирования такие дополнительные сведения могут оказаться полезными. Например, исключение 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

См. также