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

Правильная обработка исключений необходима для надежности приложений. Вы можете намеренно обрабатывать ожидаемые исключения, чтобы предотвратить сбой приложения. Однако аварийное приложение является более надежным и диагностическим, чем приложение с неопределенным поведением.

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

Обработка исключений

Ниже приведены рекомендации по обработке исключений.

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

Совет

Следующие правила анализа кода помогут найти места в коде, где можно воспользоваться этими статическими throw вспомогательными средствами: CA1510, CA1511, CA1512 и CA1513.

Если вы реализуете асинхронный метод, вызовите CancellationToken.ThrowIfCancellationRequested() вместо проверка, если отмена была запрошена, а затем создание и создание и созданиеOperationCanceledException. Дополнительные сведения см. в разделе CA2250.

Включение локализованного строкового сообщения

Сообщение об ошибке, которое видит пользователь, является производным от Exception.Message свойства создаваемого исключения, а не от имени класса исключений. Как правило, значение присваивается Exception.Message свойству путем передачи строки message сообщения в аргумент конструктора исключений.

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

Использование правильной грамматики

Составляйте понятные предложения, указывая в конце знаки препинания. Каждое предложение в строке, назначенной свойству Exception.Message, должно заканчиваться точкой. Например, "Таблица журнала переполнена". Использует правильную грамматику и знак препинания.

Хорошо разместить операторы бросить

Поместите инструкции, в которых трассировка стека будет полезной. Трассировка стека начинается в операторе, породившем исключение, и завершается оператором catch, перехватывающим это исключение.

Не вызывайте исключения в предложениях, наконец,

Не вызывайте исключения в finally предложениях. Дополнительные сведения см. в правиле анализа кода CA2219.

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

Некоторые методы, такие как Equals, GetHashCodeи ToString методы, статические конструкторы и операторы равенства, не должны вызывать исключения. Дополнительные сведения см. в правиле анализа кода CA1065.

Создание исключений проверки аргументов синхронно

В методах возврата задач необходимо проверить аргументы и вызвать все соответствующие исключения, например ArgumentException и ArgumentNullExceptionперед вводом асинхронной части метода. Исключения, создаваемые в асинхронной части метода, хранятся в возвращаемой задаче и не появляются, пока, например, задача ожидается. Дополнительные сведения см. в разделе "Исключения" в методах возврата задач.

Пользовательские типы исключений

Следующие рекомендации касаются пользовательских типов исключений:

Конечные имена классов исключений с помощью Exception

Если требуется пользовательское исключение, присвойте ему соответствующее имя и сделайте его производным от класса Exception. Например:

public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Включение трех конструкторов

При создании собственных классов исключений можно использовать по меньшей мере три общих конструктора: конструктор без параметров, конструктор, принимающий строковое сообщение, и конструктор, принимающий строковое сообщение и внутреннее исключение.

  • Exception(), использующий значения по умолчанию.
  • Exception(String), принимающий строковое сообщение.
  • Exception(String, Exception), принимающий строковое сообщение и внутреннее исключение.

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

Укажите дополнительные свойства по мере необходимости

Дополнительные сведения (кроме строки настраиваемого сообщения) включайте в исключение только в случаях, когда в соответствии со сценарием программирования такие дополнительные сведения могут оказаться полезными. Например, исключение FileNotFoundException предоставляет свойство FileName.

См. также