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

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

Использование блоков 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

См. также