关于Java:为什么我不应该在try catch中包装每个块?

Why should I not wrap every block in “try”-“catch”?

我一直相信,如果一个方法可以抛出一个异常,那么用一个有意义的try块来保护这个调用是鲁莽的。

我刚刚发布了"你应该总是把电话包装起来,这样就可以尝试一下,抓住障碍。"对于这个问题,有人告诉我这是"非常糟糕的建议"——我想知道为什么。


方法只应在能够以某种合理的方式处理异常时捕获异常。

否则,向上传递它,希望调用堆栈上更高的方法能够理解它。

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


正如米奇和其他人所说,你不应该抓住一个你不打算以某种方式处理的例外。在设计应用程序时,您应该考虑应用程序将如何系统地处理异常。这通常会导致基于抽象的错误处理层——例如,您在数据访问代码中处理所有与SQL相关的错误,这样,与域对象交互的应用程序部分就不会暴露出这样一个事实,即在某个地方有一个数据库。

除了"随处可见"的气味之外,还有一些相关的代码气味是您绝对想要避免的。

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

  • "catch,throw translated":这通常指向一个抽象问题。除非您正在实现一个联邦解决方案,将几个特定的异常转换为一个更通用的异常,否则您可能有一个不必要的抽象层…不要说"我明天可能需要它"。

  • "接住,清理,再流":这是我的一个宠物尿。如果您看到很多这样的情况,那么您应该应用资源获取IS初始化技术,并将清除部分放在一个看门人对象实例的析构函数中。

  • 我认为代码中充斥着try/catch块,是代码审查和重构的良好目标。它表明要么异常处理不被很好地理解,要么代码已成为AM&339;BA,并且严重需要重构。


    因为下一个问题是"我发现了一个例外,接下来我该怎么做?"你会怎么做?如果你什么都不做,那就是错误隐藏,程序可能"只是不工作",没有任何机会发现发生了什么。你需要明白一旦你发现了异常,你会做什么,只有当你知道的时候才会知道。


    赫伯·萨特在这里写到了这个问题。值得一读。戏弄者:

    "Writing exception-safe code is fundamentally about writing 'try' and 'catch' in the correct places." Discuss.

    Put bluntly, that statement reflects a fundamental misunderstanding of exception safety. Exceptions are just another form of error reporting, and we certainly know that writing error-safe code is not just about where to check return codes and handle error conditions.

    Actually, it turns out that exception safety is rarely about writing 'try' and 'catch' -- and the more rarely the better. Also, never forget that exception safety affects a piece of code's design; it is never just an afterthought that can be retrofitted with a few extra catch statements as if for seasoning.


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

    这有三个原因:它很方便,因为只有一个地方可以报告错误:SaveDocument()catch块。不需要在所有子方法中重复这一点,不管怎样,这就是您想要的:一个单独的地方,为用户提供关于出错的有用诊断。

    第二,每次抛出异常时都会取消保存。对于每个子方法,尝试捕捉,如果抛出异常,则进入该方法的catch块,执行离开函数,并通过SaveDocument()继续执行。如果已经出了问题,你可能会想马上停下来。

    第三,所有子方法都可以假定每次调用都成功。如果调用失败,则执行将跳转到catch块,并且不会执行后续代码。这可以使代码更清晰。例如,下面是错误代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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;
    }

    以下是例外情况下的写作方式:

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

    现在事情变得更清楚了。

    注意,异常安全代码的编写方式可能更为复杂:如果抛出异常,则不希望泄漏任何内存。确保您了解raii、stl容器、智能指针和其他在析构函数中释放资源的对象,因为对象总是在异常之前被销毁。


    如其他答案中所述,只有当您能够对异常进行某种明智的错误处理时,才应该捕获异常。

    例如,在产生问题的问题中,发问者询问是否可以安全地忽略从整数到字符串的lexical_cast的异常。这样的演员阵容永远不会失败。如果它确实失败了,程序中就出现了严重的错误。在那种情况下,你能做些什么来恢复?最好是让程序死掉,因为它处于不可信任的状态。因此,不处理异常可能是最安全的事情。


    如果您总是在可以抛出异常的方法的调用方中立即处理异常,那么异常将变得无用,您最好使用错误代码。

    异常的关键是不需要在调用链中的每个方法中处理它们。


    我听到的最好的建议是,您应该只在可以明智地对异常情况做些什么的时候捕获异常,而"捕获、记录和发布"并不是一个好策略(如果在库中偶尔不可避免的话)。


    我同意你问题的基本方向,即在最底层处理尽可能多的异常。

    现有的一些回答类似于"您不需要处理异常"。根据我的经验,这是一个不好的借口,不考虑当前开发的代码中的异常处理,使异常处理成为其他人或以后的问题。

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


    我的计算机科学教授曾经给我的建议是:"只有在无法用标准方法处理错误的情况下,才使用尝试和捕获块。"

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int f()
    {
        // Do stuff

        if (condition == false)
            return -1;
        return 0;
    }

    int condition = f();

    if (f != 0)
    {
        // handle error
    }

    然后你应该使用Try,Catch块。虽然您可以使用异常来处理这一点,但通常不建议这样做,因为异常在性能上是昂贵的。


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

    异常的目的是为了减少测试结果的频率。其思想是将异常(异常、罕见)条件从更普通的代码中分离出来。这使得普通代码更干净、更简单——但仍然能够处理这些异常情况。

    在设计良好的代码中,更深的函数可能会抛出,更高的函数可能会捕获。但关键是,许多"介于两者之间"的功能将完全摆脱处理异常情况的负担。它们只需要"例外安全",这并不意味着它们必须捕获。


    除上述建议外,我个人还使用了Try+Catch+Throw;原因如下:

  • 在不同编码器的边界上,我使用try+catch+throw输入自己编写的代码,在异常被抛出给其他人编写的调用者之前,这给了我一个机会了解我的代码中发生的一些错误情况,并且这个地方离最初抛出异常的代码更近,越近,越容易找到理由。
  • 在模块的边界,虽然不同的模块可以写我的同一个人。
  • 学习+调试的目的,在这种情况下,我使用C++中的catch(…)和C++中的catch(ExtEx),对于C++,标准库不会抛出太多的异常,所以这种情况在C++中是很少见的。但是在C,C中常见的地方有一个巨大的库和一个成熟的异常层次结构,C库代码抛出了大量的异常,理论上我(和您)应该知道您调用的函数中的每一个异常,并且知道这些异常被抛出的原因/情况,并且知道如何优雅地处理它们(通过或捕获并处理到位)。不幸的是,在我编写一行代码之前,很难了解所有可能的异常。因此,当任何异常真正发生时,我通过记录(在产品环境中)/断言对话框(在开发环境中)来捕获所有异常,并让我的代码大声说出。通过这种方式,我逐步添加异常处理代码。我知道这会与好的建议相混淆,但实际上它对我很有用,我不知道有什么更好的方法来解决这个问题。

  • 我想补充这个讨论,因为C++ 11,它确实有很多意义,只要每个EDCOX1 0块EDOCX1 1都例外,直到它可以/应该处理的点。这样就可以生成回溯。因此,我认为以前的观点部分过时了。

    使用std::nested_exceptionstd::throw_with_nested

    在stackoverflow中,这里和这里都描述了如何实现这一点。

    由于可以对任何派生的异常类执行此操作,因此可以向此类回溯添加大量信息!你也可以看看我在Github上的mwe,这里的回溯看起来像这样:

    1
    2
    3
    4
    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"

    我觉得有必要再加一个答案,尽管麦克·麦特的回答很好地总结了主要观点。我是这样想的。当你有做很多事情的方法时,你是在增加复杂性,而不是增加复杂性。

    换句话说,包装在try-catch中的方法有两个可能的结果。您有非异常结果和异常结果。当你处理很多方法的时候,这个指数级的爆炸会让你无法理解。

    指数化,因为如果每个方法以两种不同的方式分支,那么每次调用另一个方法时,都要对前面的潜在结果数进行平方。当你调用五种方法时,你至少有256种可能的结果。将其与不在每个方法中执行try/catch进行比较,您只有一条路径可以遵循。

    我就是这么看的。您可能会认为任何类型的分支都会执行相同的操作,但Try/Catch是一种特殊情况,因为应用程序的状态基本上是未定义的。

    因此,简而言之,尝试/捕获使代码更难理解。


    我有机会挽救几个项目,管理人员替换了整个开发团队,因为这个应用程序有太多的错误,用户厌倦了这些问题,四处乱跑。这些代码库都在应用程序级别进行了集中的错误处理,就像最热门的答案所描述的那样。如果这个答案是最佳实践,为什么它不起作用,并且允许以前的开发团队解决问题?也许有时候不起作用?上面的答案没有提到开发人员花了多长时间来解决单个问题。如果解决问题的时间是关键指标,那么使用try..catch块检测代码是一种更好的做法。

    我的团队如何在不显著更改用户界面的情况下解决问题?简单地说,每个方法都被检测到try..catch阻塞,并在失败点记录所有内容,方法名称、方法参数值与错误消息、错误消息、应用程序名称、日期和版本一起连接到传递的字符串中。有了这些信息,开发人员可以对错误进行分析,以确定发生最多的异常!或错误数最多的命名空间。它还可以验证模块中发生的错误是否得到了正确处理,并且不是由多个原因引起的。

    另一个好处是,开发人员可以在错误日志记录方法中设置一个断点,只需单击一个断点和"退出"调试按钮,他们就可以在故障点完全访问实际对象失败的方法中,方便地在即时窗口中找到。它使得调试非常容易,并允许将执行拖回到方法的开头,以复制问题以找到准确的行。集中异常处理是否允许开发人员在30秒内复制异常?不。

    语句"一个方法只有在它能够以某种合理的方式处理异常时才应该捕获它。"这意味着开发人员可以预测或者将遇到在发布之前可能发生的每一个错误。如果这是一个顶级的,那么就不需要应用程序异常处理程序,也就没有弹性搜索和日志存储的市场。

    这种方法还可以让开发人员发现和修复生产中的间歇性问题!是否要在生产中不使用调试程序进行调试?或者你更愿意打电话,从心烦意乱的用户那里收到电子邮件?这使您能够在其他人知道之前解决问题,而无需发送电子邮件、即时消息或在支持方面松懈,因为解决问题所需的一切都在那里。95%的问题不需要复制。

    要正常工作,需要将它与集中式日志记录结合起来,后者可以捕获命名空间/模块、类名、方法、输入和错误消息,并存储在数据库中,这样就可以对其进行聚合,以突出显示哪个方法失败得最多,从而可以首先修复它。

    有时开发人员选择从catch块向上抛出异常,但这种方法比不抛出的正常代码慢100倍。最好使用日志记录进行捕获和释放。

    这项技术被用于快速稳定一个应用程序,该应用程序在一家财富500强公司中每小时都会失败,这家公司由12个开发者开发,历时两年。使用这3000个不同的例外情况在4个月内被识别、修复、测试和部署。平均每15分钟修复一次,持续4个月。

    我同意输入所有需要插入代码的东西并不是一件有趣的事情,我更喜欢不看重复的代码,但是从长远来看,为每个方法添加4行代码是值得的。


    您不需要将代码的每个部分都隐藏在try-catch中。try-catch块的主要用途是错误处理和程序中的错误/异常。try-catch的一些用法-

  • 您可以在希望处理异常的地方使用这个块,或者简单地说,编写的代码块可能引发异常。
  • 如果您想在使用后立即处理对象,可以使用try-catch块。