【问题标题】:Fail early vs. robust methods早期失败与稳健方法
【发布时间】:2016-03-04 02:52:30
【问题描述】:

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

想象一个函数:

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

?

【问题讨论】:

    标签: robustness fail-fast-fail-early


    【解决方案1】:

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

    一般关于错误处理

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

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

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

    这也意味着在执行的根部应该有一个更通用的 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;
       }
    }
    

    【讨论】:

    • "...你应该抛出这个异常。但是你不应该捕获这个异常,因为它应该可以避免它。"这是至关重要的建议:我应该扔它而不是抓住它,而是事先防止它。我从来没有想过这种方法。非常感谢!
    猜你喜欢
    • 2017-03-04
    • 2011-02-03
    • 2018-03-04
    • 2018-05-18
    • 2010-09-23
    • 2013-10-19
    • 1970-01-01
    • 2014-09-30
    • 2023-01-22
    相关资源
    最近更新 更多