早期与强大的方法失败

时间:2015-11-30 12:52:50

标签: robustness fail-fast-fail-early

我经常(多年来)想知道实施以下最有意义的方式(对我来说这是一种悖论):

想象一个功能:

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;
    }
}

1 个答案:

答案 0 :(得分:0)

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

通常涉及错误处理

主要关注点是在可以处理它的级别上捕获异常。

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

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

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

您的示例

在关于 ArgumentNullException 的示例中:

  • 是的,你应该早点失败。每当使用无效的null参数调用方法时,都应抛出此异常。
  • 但你永远不应该抓住这个例外,因为它应该可以避免它。请参阅与此主题相关的帖子: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 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;
   }
}