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

Хорошо спроектированное приложение обрабатывает исключения и ошибки, чтобы предотвратить сбои приложения.A well-designed app handles exceptions and errors to prevent app crashes. В этом разделе описываются рекомендации по обработке и созданию исключений.This section describes best practices for handling and creating exceptions.

Использование блоков try/catch/finally для восстановления после ошибок или высвобождения ресурсовUse try/catch/finally blocks to recover from errors or release resources

Используйте блоки try/catch, выделив с их помощью код, который потенциально может явиться источником исключения, таким образом можно будет выполнить восстановление кода после возникновения этого исключения.Use try/catch blocks around code that can potentially generate an exception and your code can recover from that exception. В блоках catch следует всегда упорядочивать исключения от более производных к менее производным.In catch blocks, always order exceptions from the most derived to the least derived. Все исключения, производные от Exception.All exceptions derive from Exception. Более производные исключения не обрабатываются предложением catch, которому предшествует предложение catch для базового класса исключения.More derived exceptions are not handled by a catch clause that is preceded by a catch clause for a base exception class. Если ваш код не удается восстановить после возникновения исключения, не перехватывайте это исключение.When your code cannot recover from an exception, don't catch that exception. Включите методы выше по стеку вызовов для восстановления по мере возможности.Enable methods further up the call stack to recover if possible.

Очистите ресурсы, выделенные с помощью инструкций using или блоков finally.Clean up resources allocated with either using statements, or finally blocks. Рекомендуется использовать инструкции using для автоматической очистки ресурсов при возникновении исключений.Prefer using statements to automatically clean up resources when exceptions are thrown. Используйте блоки finally, чтобы очистить ресурсы, которые не реализуют IDisposable.Use finally blocks to clean up resources that don't implement IDisposable. Код в предложении finally выполняется почти всегда — даже при возникновении исключений.Code in a finally clause is almost always executed even when exceptions are thrown.

Обработка общих условий без выдачи исключенийHandle common conditions without throwing exceptions

Для условий, которые могут возникнуть, но способны вызвать исключение, рекомендуется реализовать обработку таким способом, который позволит избежать исключения.For conditions that are likely to occur but might trigger an exception, consider handling them in a way that will avoid the exception. Например, при попытке закрыть уже закрытое подключение возникает InvalidOperationException.For example, if you try to close a connection that is already closed, you'll get an InvalidOperationException. Этого можно избежать, используя оператор if для проверки состояния подключения перед попыткой закрыть его.You can avoid that by using an if statement to check the connection state before trying to close it.

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 можно перехватить.If you don't check connection state before closing, you can catch the InvalidOperationException exception.

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

Выбор конкретного способа зависит от того, насколько часто ожидается возникновение данного события.The method to choose depends on how often you expect the event to occur.

  • Используйте обработку исключений, если событие не происходит очень часто, то есть если событие носит действительно исключительный характер и указывает на ошибку (например, в случае неожиданного конца файла).Use exception handling if the event doesn't occur very often, that is, if the event is truly exceptional and indicates an error (such as an unexpected end-of-file). При использовании обработки исключений в обычных условиях выполняется меньше кода.When you use exception handling, less code is executed in normal conditions.

  • Если событие происходит регулярно в рамках нормальной работы программы, выполняйте проверку на наличие ошибок прямо в коде.Check for error conditions in code if the event happens routinely and could be considered part of normal execution. Проверка на наличие распространенных условий ошибки позволяет выполнять меньший объем кода благодаря устранению исключений.When you check for common error conditions, less code is executed because you avoid exceptions.

Устранение исключений при разработке классовDesign classes so that exceptions can be avoided

Класс может предоставлять методы и свойства, позволяющие избежать вызова, способного выдать исключение.A class can provide methods or properties that enable you to avoid making a call that would trigger an exception. Например, класс FileStream содержит методы, позволяющие определить, достигнут ли конец файла.For example, a FileStream class provides methods that help determine whether the end of the file has been reached. Это позволяет избежать появления исключения, создаваемого в случае выполнения чтения после окончания файла.These can be used to avoid the exception that is thrown if you read past the end of the file. В следующем примере показан способ чтения до конца файла без выдачи исключения.The following example shows how to read to the end of a file without triggering an exception.

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 (или значение по умолчанию).Another way to avoid exceptions is to return null (or default) for extremely common error cases instead of throwing an exception. Такие ошибки могут относиться к обычному потоку управления.An extremely common error case can be considered normal flow of control. Возвращая значение NULL (или значение по умолчанию) в таких случаях, можно уменьшить влияние на производительность приложения.By returning null (or default) in these cases, you minimize the performance impact to an app.

При выборе типа значения Nullable<T> или значения по умолчанию в качестве индикатора ошибки учитывайте особенности приложения.For value types, whether to use Nullable<T> or default as your error indicator is something to consider for your particular app. При использовании Nullable<Guid> default принимает значение null, а не Guid.Empty.By using Nullable<Guid>, default becomes null instead of Guid.Empty. В некоторых случаях добавление Nullable<T> помогает более точно определить, присутствует или отсутствует значение.Some times, adding Nullable<T> can make it clearer when a value is present or absent. Но в определенных ситуациях добавление Nullable<T> может привести к созданию лишних необязательных случаев для проверки, что повышает вероятность ошибки.Other times, adding Nullable<T> can create extra cases to check that aren't necessary, and only serve to create potential sources of errors.

Выдача исключений вместо возврата кода ошибкиThrow exceptions instead of returning an error code

Исключения гарантируют, что сбои не останутся незамеченными из-за того, что вызывающий код не проверил код возврата.Exceptions ensure that failures do not go unnoticed because calling code didn't check a return code.

Использование предопределенных типов исключений .NETUse the predefined .NET exception types

Создавайте новый класс исключений, только если предопределенное исключение не подходит.Introduce a new exception class only when a predefined one doesn't apply. Например:For example:

  • Вызывайте исключение InvalidOperationException, если значение свойства или вызов метода не соответствуют текущему состоянию объекта.Throw an InvalidOperationException exception if a property set or method call is not appropriate given the object's current state.

  • Порождайте исключение ArgumentException или одного из предварительно определенных классов, которые являются производными от ArgumentException, если передаются недопустимые параметры.Throw an ArgumentException exception or one of the predefined classes that derive from ArgumentException if invalid parameters are passed.

Завершайте имена классов исключений словом ExceptionEnd exception class names with the word Exception

Если требуется пользовательское исключение, присвойте ему соответствующее имя и сделайте его производным от класса Exception.When a custom exception is necessary, name it appropriately and derive it from the Exception class. Например:For example:

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

Включение трех конструкторов в пользовательские классы исключенийInclude three constructors in custom exception classes

При создании собственных классов исключений можно использовать по меньшей мере три общих конструктора: конструктор без параметров, конструктор, принимающий строковое сообщение, и конструктор, принимающий строковое сообщение и внутреннее исключение.Use at least the three common constructors when creating your own exception classes: the parameterless constructor, a constructor that takes a string message, and a constructor that takes a string message and an inner exception.

Пример см. в статье Практическое руководство. Создание пользовательских исключений.For an example, see How to: Create User-Defined Exceptions.

Обеспечение доступности данных об исключении при удаленном выполнении кодаEnsure that exception data is available when code executes remotely

При создании пользовательских исключений следует обеспечить доступность метаданных исключений для удаленно исполняемого кода.When you create user-defined exceptions, ensure that the metadata for the exceptions is available to code that is executing remotely.

Например, для реализаций .NET, которые поддерживают домены приложений, могут возникать исключения для этих доменов.For example, on .NET implementations that support App Domains, exceptions may occur across App domains. Предположим, что домен приложения А создает домен приложения В, который выполняет код, вызывающий исключение.Suppose App Domain A creates App Domain B, which executes code that throws an exception. Чтобы домен приложения A правильно перехватил и обработал исключение, он должен найти сборку, которая содержит исключение, порожденное доменом приложения B. Если домен приложения B порождает исключение, содержащееся в сборке в его базовой папке приложения, но не в базовой папке приложения домена A, то домен приложения A не сможет найти исключение и среда CLR породит исключение FileNotFoundException.For App Domain A to properly catch and handle the exception, it must be able to find the assembly that contains the exception thrown by App Domain B. If App Domain B throws an exception that is contained in an assembly under its application base, but not under App Domain A's application base, App Domain A will not be able to find the exception, and the common language runtime will throw a FileNotFoundException exception. Чтобы избежать такой ситуации, можно развернуть сборку, содержащую сведения об исключении, двумя способами:To avoid this situation, you can deploy the assembly that contains the exception information in two ways:

  • Поместите эту сборку в общую базу приложения, совместно используемую обоими доменами приложений.Put the assembly into a common application base shared by both app domains.

    - или -- or -

  • Если у этих доменов нет общей базы приложения, то подпишите сборку, содержащую сведения об исключении, строгим именем и разверните ее в глобальном кэше сборок.If the domains do not share a common application base, sign the assembly that contains the exception information with a strong name and deploy the assembly into the global assembly cache.

Использование грамматически правильных сообщений об ошибкеUse grammatically correct error messages

Составляйте понятные предложения, указывая в конце знаки препинания.Write clear sentences and include ending punctuation. Каждое предложение в строке, назначенной свойству Exception.Message, должно заканчиваться точкой.Each sentence in the string assigned to the Exception.Message property should end in a period. Например, "Таблица журнала переполнена."For example, "The log table has overflowed." будет подходящей строкой сообщения.would be an appropriate message string.

Включение локализованной строки сообщения в каждое исключениеInclude a localized string message in every exception

Сообщение об ошибке, показываемое пользователю, извлекается из свойства Exception.Message созданного исключения, а не из имени класса исключения.The error message that the user sees is derived from the Exception.Message property of the exception that was thrown, and not from the name of the exception class. Как правило, вы присваиваете значение свойству Exception.Message, передав строку сообщения аргументу message конструктора исключений.Typically, you assign a value to the Exception.Message property by passing the message string to the message argument of an Exception constructor.

Для локализованных приложений необходимо предоставить строку локализованного сообщения для всех исключений, которые может создавать приложение.For localized applications, you should provide a localized message string for every exception that your application can throw. Используйте файлы ресурсов для предоставления локализованных сообщений об ошибках.You use resource files to provide localized error messages. Сведения о локализации приложений и извлечении локализованных строк см. в следующих статьях:For information on localizing applications and retrieving localized strings, see the following articles:

Предоставление дополнительных свойств в пользовательских исключениях по мере необходимостиIn custom exceptions, provide additional properties as needed

Дополнительные сведения (кроме строки настраиваемого сообщения) включайте в исключение только в случаях, когда в соответствии со сценарием программирования такие дополнительные сведения могут оказаться полезными.Provide additional properties for an exception (in addition to the custom message string) only when there's a programmatic scenario where the additional information is useful. Например, исключение FileNotFoundException предоставляет свойство FileName.For example, the FileNotFoundException provides the FileName property.

Размещение операторов throw для удобной трассировки стекаPlace throw statements so that the stack trace will be helpful

Трассировка стека начинается в операторе, породившем исключение, и завершается оператором catch, перехватывающим это исключение.The stack trace begins at the statement where the exception is thrown and ends at the catch statement that catches the exception.

Использование методов построителя исключенийUse exception builder methods

Обычно класс генерирует одно и то же исключение из различных мест своей реализации.It is common for a class to throw the same exception from different places in its implementation. Чтобы избежать повторения кода, используйте вспомогательные методы, создающие исключение и затем возвращающие его.To avoid excessive code, use helper methods that create the exception and return it. Например:For example:

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

В некоторых случаях для создания исключения лучше воспользоваться конструктором исключений.In some cases, it's more appropriate to use the exception's constructor to build the exception. В качестве примера можно привести класс глобальных исключений, например ArgumentException.An example is a global exception class such as ArgumentException.

Восстановление состояния, если методы не выполняются из-за исключенияRestore state when methods don't complete due to exceptions

Вызывающие объекты должны предполагать, что при создании исключения из метода не возникают побочные эффекты.Callers should be able to assume that there are no side effects when an exception is thrown from a method. Например, если у вас есть код, который передает деньги, списывая их с одного счета и внося на другой, и при начислении средств возникает исключение, списание средств применяться не должно.For example, if you have code that transfers money by withdrawing from one account and depositing in another account, and an exception is thrown while executing the deposit, you don't want the withdrawal to remain in effect.

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

Описанный выше метод непосредственно не создает исключения, однако при его написании необходимо соблюдать осторожность, чтобы при сбое операции начисления списание отменялось.The method above does not directly throw any exceptions, but must be written defensively so that if the deposit operation fails, the withdrawal is reversed.

Один из способов обработки в этой ситуации заключается в перехвате всех исключений, выданных транзакцией начисления средств, и откате транзакции списания средств.One way to handle this situation is to catch any exceptions thrown by the deposit transaction and roll back the withdrawal.

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.This example illustrates the use of throw to re-throw the original exception, which can make it easier for callers to see the real cause of the problem without having to examine the InnerException property. Альтернативным способом является выдача нового исключения с включением исходного исключения в качестве внутреннего:An alternative is to throw a new exception and include the original exception as the inner exception:

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

См. такжеSee also