尽早失败与稳健的方法

Fail early vs. robust methods

我一直(多年来)想知道实现以下内容的最有意义的方法(这对我来说有点自相矛盾):

想象一个函数:

DoSomethingWith(value)
{
    if (value == null) { // Robust: Check parameter(s) first
        throw new ArgumentNullException(value);
    }

    // Some code ...
}

它的名字是这样的:

SomeFunction()
{
    if (value == null) { // Fail early
        InformUser();

        return;
    }

    DoSomethingWith(value);
}

但是,要捕获 ArgumentNullException,我应该这样做:

SomeFunction()
{
    if (value == null) { // Fail early
        InformUser();

        return;
    }

    try { // If throwing an Exception, why not *not* check for it (even if you checked already)?
        DoSomethingWith(value);
    } catch (ArgumentNullException) {
        InformUser();

        return;
    }
}

或者只是:

SomeFunction()
{
    try { // No fail early anymore IMHO, because you could fail before calling DoSomethingWith(value)
        DoSomethingWith(value);
    } catch (ArgumentNullException) {
        InformUser();

        return;
    }
}

?

这是一个非常普遍的问题,正确的解决方案取决于具体的代码和架构。

一般关于错误处理

主要重点应该是在您可以处理的级别上捕获异常。

在正确的地方处理异常使代码健壮,因此异常不会使应用程序失败并且可以相应地处理异常。

尽早失败会使应用程序健壮,因为这有助于避免不一致的状态。

这也意味着在执行的根部应该有一个更通用的 try catch 块,以捕获任何无法处理的非致命应用程序错误。这通常意味着您作为程序员没有想到这个错误源。稍后您可以扩展您的代码以处理此错误。但是执行根不应该是你想到异常处理的唯一地方。

你的例子

在您关于 ArgumentNullException 的示例中:

  • 是的,你应该早点失败。每当使用无效的 null 参数调用您的方法时,您应该抛出此异常。
  • 但是你永远不应该捕获这个异常,因为它应该是可以避免的。请参阅与主题相关的 post:If catching null pointer exception is not a good practice, is catching exception a good one?
  • 如果您正在处理用户输入或来自其他系统的输入,那么您应该验证输入。例如。您可以在空检查后显示 UI 上的验证错误而不抛出异常。如何向用户显示问题始终是错误处理的关键部分,因此请为您的应用程序定义适当的策略。您应该尽量避免在预期的程序执行流程中抛出异常。看这篇文章:https://msdn.microsoft.com/en-us/library/ms173163.aspx

查看下面关于异常处理的一般想法:

在你的方法中处理异常

如果在 DoSomethingWith 方法中抛出异常并且您可以处理它并继续执行流程而没有任何问题,那么您当然应该这样做。

这是重试数据库操作的伪代码示例:

void DoSomethingAndRetry(value)
{
   try
   {
       SaveToDatabase(value);
   }
   catch(DeadlockException ex)
   {
       //deadlock happened, we are retrying the SQL statement
       SaveToDatabase(value);
   }
}

让异常冒泡

让我们假设您的方法是 public。如果发生异常意味着该方法失败并且您无法继续执行,那么异常应该冒泡,以便调用代码可以相应地处理它。这取决于调用代码如何对异常做出反应的用例。

在让异常冒泡之前,您可以将其包装到另一个特定于应用程序的异常中作为内部异常以添加额外的上下文信息。您也可以以某种方式处理异常,例如记录它,或者将日志记录留给调用代码,具体取决于您的日志记录架构。

public bool SaveSomething(value)
{
   try 
   {
       SaveToFile(value);
   }
   catch(FileNotFoundException ex)
   {
       //process exception if needed, E.g. log it
       ProcessException(ex);
       //you may want to wrap this exception into another one to add context info
       throw WrapIntoNewExceptionWithSomeDetails(ex);
   }
}

记录可能的异常情况

在 .NET 中,定义您的方法抛出哪些异常以及可能抛出异常的原因也很有帮助。这样调用代码就可以考虑到这一点。参见 https://msdn.microsoft.com/en-us/library/w1htk11d.aspx

示例:

/// <exception cref="System.Exception">Thrown when something happens..</exception>
DoSomethingWith(value)
{
   ...
}

忽略异常

对于您可以接受失败方法并且不想一直在其周围添加 try catch 块的方法,您可以创建一个具有类似签名的方法:

public bool TryDoSomethingWith(value)
{
   try 
   {
       DoSomethingWith(value);
       return true;
   }
   catch(Exception ex)
   {
        //process exception if needed, e.g. log it
        ProcessException(ex);
        return false;
   }
}