异常的最佳做法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. 所有异常都派生自 ExceptionAll 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. 例如,如果尝试关闭已关闭的连接,则会获得 InvalidOperationExceptionFor 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.EmptyBy 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.

使用预定义的 .NET 异常类型Use the predefined .NET exception types

仅当预定义的异常类不适用时,引入新异常类。Introduce a new exception class only when a predefined one doesn't apply. 例如:For example:

异常类名称的结尾为 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. 假设应用域 A 创建应用域 B,后者执行引发异常的代码。Suppose App Domain A creates App Domain B, which executes code that throws an exception. 应用域 A 若想正确捕获和处理异常,它必须能够找到包含应用域 B 所引发的异常的程序集。如果应用域 B 在其应用程序基下(但未在应用域 A 的应用程序基下)引发了一个包含在程序集内的异常,那么应用域 A 将无法找到异常,且公共语言运行时将引发 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.

放置引发语句,使得堆栈跟踪有所帮助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