首页 文章

我为什么不在“尝试” - “捕获”中包裹每个块?

提问于
浏览
405

我一直认为,如果一个方法可以抛出一个异常,那么不顾及用一个有意义的try块来保护这个调用 .

我刚刚发布了'你应该总是包装可以投入试试的拦截器 . 到this question并被告知这是'remarkably bad advice' - 我想了解原因 .

15 回答

  • 130

    一个方法应该只能在以某种合理的方式处理异常时捕获异常 .

    否则,将其传递给up,希望调用堆栈上方的方法可以理解它 .

    正如其他人所指出的那样,最好在调用堆栈的最高级别拥有一个未处理的异常处理程序(带有日志记录),以确保记录任何致命错误 .

  • 14

    Mitch and others所述,您不应该捕获一个您不打算以某种方式处理的例外情况 . 您应该考虑应用程序在设计时如何系统地处理异常 . 这通常会导致基于抽象的错误处理层 - 例如,您处理数据访问代码中的所有SQL相关错误,以便与域对象交互的应用程序部分不会暴露于那里的事实某个地方是引擎盖下的DB .

    除了"catch everything everywhere"气味外,还有一些相关的代码气味,你绝对要避免 .

    • "catch, log, rethrow":如果您想要基于作用域的日志记录,那么当堆栈因异常(ala std::uncaught_exception() )展开时,编写一个在其析构函数中发出日志语句的类 . 您需要做的就是在您感兴趣的范围内声明一个日志记录实例,瞧,您有日志记录,没有不必要的 try / catch 逻辑 .

    • "catch, throw translated":这通常指向抽象问题 . 除非您正在实现一个联合解决方案,您将几个特定异常转换为一个更通用的异常,否则您可能有一个不必要的抽象层......并且不要说"I might need it tomorrow" .

    • "catch, cleanup, rethrow":这是我的一个小小的烦恼 . 如果您看到很多这样的内容,那么您应该应用Resource Acquisition is Initialization技术并将清理部分放在janitor对象实例的析构函数中 .

    我认为充满 try / catch 块的代码是代码审查和重构的良好目标 . 它表明任何一个异常处理都没有得到很好的理解,或者代码已成为一个amœba并且非常需要重构 .

  • 24

    因为下一个问题是“我发现了一个例外,我接下来该怎么做?”你会怎么做?如果你什么也不做 - 这就是隐藏的错误,程序可能“没有工作”,没有任何机会找到发生的事情 . 一旦你发现异常,你需要了解你将会做些什么,如果你知道的话,你只需要 grab .

  • 309

    Herb Sutter写了这个问题here . 当然值得一读 .
    预告片:

    “编写异常安全的代码基本上就是在正确的位置编写'try'和'catch' . ”讨论 . 坦率地说,这一陈述反映了对异常安全的根本误解 . 例外只是错误报告的另一种形式,我们当然知道编写错误安全代码不仅仅是检查返回代码和处理错误条件的位置 . 事实上,事实证明异常安全很少是写'尝试'和'捕获' - 而且越少越好 . 此外,永远不要忘记异常安全会影响一段代码的设计;它不仅仅是一个事后的想法,可以用一些额外的捕获声明进行改装,就好像用于调味一样 .

  • 23

    您不需要使用try-catches覆盖每个块,因为try-catch仍然可以捕获调用堆栈中的函数中抛出的未处理异常 . 因此,不是让每个函数都有一个try-catch,你可以在应用程序的顶级逻辑中使用一个 . 例如,可能有一个 SaveDocument() 顶级例程,它调用许多调用其他方法等的方法 . 这些子方法仍然被 SaveDocument() 捕获 .

    这很好,有三个原因:它很方便,因为你只有一个地方可以报告错误: SaveDocument() catch块 . 无论如何,'s no need to repeat this throughout all the sub-methods, and it'是你想要的:一个单一的地方可以让用户对出错的东西进行有用的诊断 .

    第二,每当抛出异常时都会取消保存 . 随着每个子方法尝试捕获,如果抛出异常,你进入该方法的catch块,执行离开函数,并继续通过 SaveDocument() . 如果事情已经出错了你可能想要在那里停下来 .

    三,所有子方法都可以假设每次调用都成功 . 如果调用失败,执行将跳转到catch块,后续代码永远不会执行 . 这可以使您的代码更清晰 . 例如,这里有错误代码:

    int ret = SaveFirstSection();
    
    if (ret == FAILED)
    {
        /* some diagnostic */
        return;
    }
    
    ret = SaveSecondSection();
    
    if (ret == FAILED)
    {
        /* some diagnostic */
        return;
    }
    
    ret = SaveThirdSection();
    
    if (ret == FAILED)
    {
        /* some diagnostic */
        return;
    }
    

    以下是使用例外编写的方法:

    // these throw if failed, caught in SaveDocument's catch
    SaveFirstSection();
    SaveSecondSection();
    SaveThirdSection();
    

    现在它很多发生了什么事情更清楚 .

    注意异常安全代码以其他方式编写可能更难:如果抛出异常,您不希望泄漏任何内存 . 确保您了解 RAII ,STL容器,智能指针以及在析构函数中释放其资源的其他对象,因为对象始终在异常之前被销毁 .

  • 0

    如其他答案中所述,如果您可以对其进行某种合理的错误处理,则应该只捕获异常 .

    例如,在产生问题的the question中,提问者询问是否可以安全地忽略 lexical_cast 从整数到字符串的异常 . 这样的演员应该永远不会失败 . 如果确实失败了,那么程序就会出现严重错误 . 在那种情况下你可以做些什么来恢复?它是受信任的's probably best to just let the program die, as it is in a state that can' . 因此,不处理异常可能是最安全的事情 .

  • 9

    如果你总是在一个可以引发异常的方法的调用者中立即处理异常,那么异常变得无用,你最好使用错误代码 .

    异常的全部意义在于它们不需要在调用链中的每个方法中处理 .

  • 5

    我听过的最好的建议是你应该只在你可以明智地对异常情况做些什么的时候捕捉异常,并且“捕获,记录和释放”不是一个好的策略(如果在图书馆偶尔不可避免) .

  • 1

    我同意你的问题的基本方向,以尽可能多地处理最低级别的例外情况 .

    一些现有的答案就像"You don't need to handle the exception. Someone else will do it up the stack."我的经验是 bad excuse to not think 关于当前开发的代码中的异常处理,使异常处理别人或以后的问题 .

    在分布式开发中,这个问题会急剧增加,您可能需要调用同事实现的方法 . 然后你必须检查一个嵌套的方法调用链,找出他/她为什么抛出一些异常的原因,这可以在最深的嵌套方法中更容易处理 .

  • -2

    我的计算机科学教授给我的建议是:“只有在无法使用标准方法处理错误时才使用Try和Catch块 . ”

    作为一个例子,他告诉我们,如果一个程序在一个不可能做的事情的地方遇到一些严重的问题:

    int f()
    {
        // Do stuff
    
        if (condition == false)
            return -1;
        return 0;
    }
    
    int condition = f();
    
    if (f != 0)
    {
        // handle error
    }
    

    然后你应该使用try,catch块 . 虽然您可以使用异常来处理此问题,但通常不建议这样做,因为异常是性能代价高昂的 .

  • 45

    如果要测试每个函数的结果,请使用返回码 .

    例外的目的是让您可以经常测试结果 . 我们的想法是从更普通的代码中分离出异常(不寻常,罕见)的条件 . 这使普通代码更清晰,更简单 - 但仍然能够处理这些异常情况 .

    在设计良好的代码中,可能会抛出更深层的函数,而更高级的函但关键是,“介于两者之间”的许多功能将完全免除处理特殊情况的负担 . 他们只需要“例外安全”,这并不意味着他们必须 grab .

  • 6

    除了以上建议,我个人尝试一下 grab ;原因如下:

    • 在不同编码器的边界,我在自己写的代码中使用try catch throw,在异常被抛给其他人编写的调用者之前,这让我有机会知道我的代码中出现了一些错误情况,这个place更接近最初抛出异常的代码,越接近,越容易找到原因 .

    • 在模块的边界,虽然不同的模块可能写在同一个人身上 .

    • 学习调试的目的,在这种情况下我在C中使用catch(...)并在C#中捕获(Exception ex),对于C,标准库不会抛出太多异常,因此这种情况在C中很少见 . 但是在C#中常见的地方,C#有一个庞大的库和一个成熟的异常层次结构,C#库代码抛出了大量的异常,理论上我(和你)应该知道你调用的函数的每个异常,并知道原因/案例为什么这些异常被抛出,并且知道如何处理它们(优雅地传递或捕获并就地处理它) . 不幸的是,实际上它已经知道了解决这个问题的更好方法 .

  • 10

    我想补充一下这个讨论, since C++11 ,它确实很有意义,只要每个 catch 阻止 rethrow 是例外,直到可以/应该处理它为止 . 这样 a backtrace can be generated . 因此,我认为以前的意见已经过时了 .

    使用std :: nested_exception和std :: throw_with_nested

    它在StackOverflow herehere上有所描述,如何实现这一点 .

    由于您可以使用任何派生的异常类执行此操作,因此可以向此类回溯添加大量信息!您也可以查看我的MWE on GitHub,其中有一个回溯看起来像这样:

    Library API: Exception caught in function 'api_function'
    Backtrace:
    ~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
    ~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"
    
  • 1

    如果要轻松修复间歇性 生产环境 问题,则应将每个代码块包装在try..catch块中 . 这基本上是对代码的检测,目的是提供广泛的调试信息,允许您在 生产环境 中无需调试器进行调试 . 用户无需通过电子邮件或与支持人员聊天,解决问题所需的所有信息就在那里 . 没有必要重现问题 .

    要正常工作,需要结合广泛的日志记录,这些日志记录可以捕获命名空间/模块,类名,方法,输入和错误消息并存储在数据库中,以便可以聚合以突出显示哪个方法失败最多,因此它可以是先修好了 .

    例外情况比普通代码慢100到1000倍,永远不应该重新抛出 . 也不要创建异常并抛出它 . 这非常令人厌恶 . 捕获异常,因此可以使用常规代码修复 .

    这项技术被用于快速稳定由12个Devs开发的财富500强公司中的一个有缺陷的应用程序超过2年 . 使用这个我识别,修复,构建测试,并在4个月内部署3000个修复程序,在这种情况下系统不再报告任何异常,因为所有处理 . 这平均每15分钟平均修复4个月 .

  • 2

    您无需在 try-catch 中覆盖代码的每个部分 . try-catch 块的主要用途是错误处理并在程序中出现错误/异常 . try-catch 的一些用法 -

    • 您可以在要处理异常的位置使用此块,或者只是可以说编写的代码块可能会引发异常 .

    • 如果要在使用后立即处置对象,可以使用 try-catch 块 .

相关问题