【问题标题】:Why should I not wrap every block in "try"-"catch"?为什么我不应该将每个块都包装在“try”-“catch”中?
【发布时间】:2011-02-13 19:19:41
【问题描述】:

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

我刚刚向this question 发布了“你应该总是包装可以抛出 try、catch 块的调用。”并被告知这是“非常糟糕的建议”——我想明白为什么。

【问题讨论】:

    标签: exception language-agnostic try-catch


    【解决方案1】:

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

    否则,将其向上传递,希望调用堆栈更高的方法可以理解它。

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

    【讨论】:

    • 还值得注意的是,try 块有成本(就生成的代码而言)。 Scott Meyers 的“More Effective C++”中有很好的讨论。
    • 实际上try 块在任何现代 C 编译器中都是免费的,该信息的日期为 Nick。我也不同意拥有顶级异常处理程序,因为您会丢失位置信息(指令失败的实际位置)。
    • @Blindly: 顶级异常处理器不是用来处理异常,而是大声喊出有一个未处理的异常,给出它的消息,然后优雅地结束程序(返回 1 而不是调用 terminate)。它更多的是一种安全机制。此外,try/catch 在没有任何异常的情况下或多或少是免费的。当有一个传播时,它每次被抛出和捕获都会消耗时间,因此只有重新抛出的 try/catch 链并不是没有成本的。
    • 我不同意你应该总是在未捕获的异常上崩溃。现代软件设计是非常分割的,那么你为什么要仅仅因为一个错误就惩罚应用程序的其余部分(更重要的是用户!)?崩溃是你最不想做的事情,至少尝试给用户一些小的代码窗口,即使无法访问应用程序的其余部分,也可以让他们节省工作。
    • Kendall:如果异常到达顶级处理程序,则根据定义,您的应用程序处于未定义状态。尽管在某些特定情况下,保留用户数据可能很有价值(想到 Word 的文档恢复),但程序不应覆盖任何文件或提交到数据库。
    【解决方案2】:

    正如Mitch and others 所说,您不应该捕获您不打算以某种方式处理的异常。在设计应用程序时,您应该考虑应用程序将如何系统地处理异常。这通常会导致基于抽象的错误处理层——例如,您在数据访问代码中处理所有与 SQL 相关的错误,这样与域对象交互的应用程序部分就不会暴露于以下事实:是某个地方的数据库。

    除了“随处可见”异味之外,还有一些您绝对要避免的相关代码异味。

    1. "catch, log, rethrow":如果您想要基于范围的日志记录,则编写一个类,当堆栈因异常而展开时(阿拉std::uncaught_exception())。您需要做的就是在您感兴趣的范围内声明一个日志记录实例,瞧,您有日志记录并且没有不必要的try/catch 逻辑。

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

    3. “抓、清理、再扔”:这是我最讨厌的事情之一。如果您看到很多这样的情况,那么您应该应用Resource Acquisition is Initialization 技术并将清理部分放在 janitor 对象实例的析构函数中。

    我认为充斥着try/catch 块的代码是代码审查和重构的好目标。这表明要么异常处理没有得到很好的理解,要么代码已经变成了变形虫,急需重构。

    【讨论】:

    • #1 对我来说是新的。为此+1。另外,我想指出 #2 的一个常见异常,如果您经常设计一个库,您会希望将内部异常转换为库接口指定的内容以减少耦合(这可能是您的意思通过“联合解决方案”,但我不熟悉该术语)。
    • #2,它不是代码异味但很有意义,可以通过将旧异常保留为嵌套异常来增强。
    • 关于#1:std::uncaught_exception() 告诉您有一个未捕获的异常正在运行,但AFAIK 只有一个 catch() 子句可以让您确定该异常实际上是什么。因此,虽然您可以记录由于未捕获的异常而退出范围的事实,但只有封闭的 try/catch 可以让您记录任何详细信息。对吗?
    • @Jeremy - 你是对的。我通常在处理异常时记录异常详细信息。跟踪中间帧非常有用。您通常还需要记录线程标识符或一些识别上下文以关联日志行。我使用了类似于log4j.LoggerLogger 类,它在每个日志行中都包含线程ID,并在异常处于活动状态时在析构函数中发出警告。
    【解决方案3】:

    因为下一个问题是“我发现了一个异常,接下来我该怎么做?”你会怎么做?如果您什么都不做 - 那就是错误隐藏,程序可能“无法正常工作”而没有任何机会找到发生的事情。您需要了解捕获异常后您将执行的具体操作,并且只有在您知道时才捕获。

    【讨论】:

      【解决方案4】:

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

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

      第二,只要抛出异常,就会取消保存。对于每个子方法的尝试捕获,如果抛出异常,您将进入该方法的 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 容器、智能指针和其他在析构函数中释放资源的对象,因为对象总是在异常之前被破坏。

      【讨论】:

      • 出色的例子。是的,在逻辑单元中尽可能高地捕获,例如围绕一些“事务”操作,如加载/保存/等。没有什么比充斥着重复的、冗余的 try-catch 块的代码更糟糕的了退出!如果发生值得异常的故障,我敢打赌大多数用户只想挽救他们可以做的事情,或者至少不想处理关于它的 10 级消息。
      • 只是想说这是我读过的最好的“早扔,晚赶”的解释之一:简洁,例子完美地说明了你的观点。谢谢!
      【解决方案5】:

      Herb Sutter 写了关于这个问题的文章 here。绝对值得一读。
      预告片:

      “编写异常安全的代码基本上就是在正确的地方编写‘try’和‘catch’。”讨论。

      坦率地说,该声明反映了对异常安全的根本误解。异常只是错误报告的另一种形式,我们当然知道编写错误安全代码不仅仅是检查返回码和处理错误条件的位置。

      实际上,异常安全性很少与编写“try”和“catch”有关,而且越少越好。另外,永远不要忘记异常安全会影响一段代码的设计。它绝不是事后才想到的,可以通过一些额外的 catch 语句进行改造,就像调味一样。

      【讨论】:

        【解决方案6】:

        如其他答案所述,只有在可以对其进行某种合理的错误处理时,才应捕获异常。

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

        【讨论】:

          【解决方案7】:

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

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

          【讨论】:

            【解决方案8】:

            我听到的最好的建议是,您应该只在可以明智地对异常情况采取措施的地方捕获异常,并且“捕获、记录和释放”不是一个好策略(如果在某些情况下偶尔无法避免)图书馆)。

            【讨论】:

            • @KeithB:我认为这是次优策略。如果你能用另一种方式写日志就更好了。
            • @KeithB:这是一个“在图书馆里总比没有好”的策略。 “捕捉、记录、妥善处理”在可能的情况下会更好。 (是的,我知道这并不总是可能的。)
            【解决方案9】:

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

            我的团队如何在不显着更改 UI 的情况下解决问题?很简单,每个方法都使用 try..catch 进行检测,并在故障点记录所有内容,方法名称、方法参数值与错误消息、错误消息、应用程序名称、日期、和版本。有了这些信息,开发人员可以对错误进行分析,以确定发生最多的异常!或者错误数量最多的命名空间。它还可以验证模块中发生的错误是否得到妥善处理,而不是由多种原因引起的。

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

            语句“一个方法应该只在它能够以某种合理的方式处理异常时才捕获它。”这意味着开发人员可以预测或将遇到在发布之前可能发生的每个错误。如果这是真正的顶级,则不需要应用程序异常处理程序,并且 Elastic Search 和 Logstash 将没有市场。

            这种方法还可以让开发人员发现并修复生产中的间歇性问题!您想在生产环境中不使用调试器进行调试吗?还是您宁愿接听电话并收到来自心烦意乱的用户的电子邮件?这使您可以在其他人知道之前解决问题,而无需通过电子邮件、IM 或 Slack 获得支持,因为解决问题所需的一切都在那里。 95% 的问题永远不需要重现。

            为了正常工作,它需要与可以捕获命名空间/模块、类名、方法、输入和错误消息并存储在数据库中的集中式日志记录相结合,以便可以汇总以突出显示哪个方法最失败,所以可以先修好。

            有时开发人员会选择从 catch 块中将异常向上抛出堆栈,但这种方法比不抛出的普通代码慢 100 倍。使用日志记录捕获和释放是首选。

            在 12 位开发人员历时 2 年开发的财富 500 强公司中,该技术用于快速稳定大多数用户每小时都会失败的应用。使用这 3000 个不同的异常在 4 个月内被识别、修复、测试和部署。这平均每 15 分钟修复一次,持续 4 个月。

            我同意输入检测代码所需的所有内容并不有趣,我更喜欢不查看重复的代码,但从长远来看,为每个方法添加 4 行代码是值得的。

            【讨论】:

            • 包装每个块似乎有点矫枉过正。它很快就会使您的代码变得臃肿且难以阅读。从更高级别的异常记录堆栈跟踪可以向您显示问题发生的位置以及与错误本身相结合通常足以继续进行的信息。我很好奇你在哪里发现这还不够。只是为了我能获得别人的经验。
            • “异常比普通代码慢 100 到 1000 倍,并且永远不应该被重新抛出”——这句话在大多数现代编译器和硬件上是不正确的。
            • 这似乎有点矫枉过正,需要一些输入,但这是对异常执行分析以首先查找和修复最大错误(包括生产中的间歇性错误)的唯一方法。如果需要,catch 块会处理特定的错误,并有一行代码记录。
            • 不,异常非常慢。另一种方法是返回代码、对象或变量。请参阅此堆栈溢出帖子...“异常至少比返回码慢 30,000 倍”stackoverflow.com/questions/891217/…
            【解决方案10】:

            我同意您问题的基本方向,即在最低级别处理尽可能多的异常。

            一些现有的答案类似于“您不需要处理异常。其他人会在堆栈中完成它。”根据我的经验,这是一个不考虑当前开发的代码段的异常处理的糟糕借口,让异常处理其他人或以后的问题。

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

            【讨论】:

              【解决方案11】:

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

              例如,他告诉我们,如果程序在无法执行以下操作的地方遇到严重问题:

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

              那么你应该使用 try, catch 块。虽然您可以使用异常来处理此问题,但通常不建议这样做,因为异常在性能方面代价高昂。

              【讨论】:

              • 这是一种策略,但许多人建议从不从函数返回错误代码或失败/成功状态,而是使用异常。基于异常的错误处理通常比基于错误代码的代码更容易阅读。 (有关示例,请参阅 AshleysBrain 对此问题的回答。)另外,请始终记住,许多计算机科学教授几乎没有编写真正代码的经验。
              • -1 @Sagelika 你的答案在于避免异常,所以不需要try-catch。
              • @Kristopher:返回码的其他大缺点是很容易忘记检查返回码,而且在调用之后不一定是处理问题的最佳位置。
              • 嗯,这取决于,但在许多情况下(抛开那些真正不应该抛出的人),由于很多原因,异常优于返回代码。在大多数情况下,异常对性能有害的想法是一个很大的'[需要引用]
              【解决方案12】:

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

              例外的目的是让您可以更少地测试结果。这个想法是从你更普通的代码中分离出异常(不寻常的、罕见的)条件。这使普通代码更简洁,但仍然能够处理那些异常情况。

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

              【讨论】:

                【解决方案13】:

                我想在这个讨论中补充一点,从 C++11 开始,它确实很有意义,只要每个 catch 阻止 rethrows 异常直到它可以/应该处理的点。这样可以生成回溯。因此,我认为之前的观点部分已经过时了。

                使用std::nested_exceptionstd::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"
                

                【讨论】:

                  【解决方案14】:

                  我觉得有必要添加另一个答案,尽管 Mike Wheat 的回答很好地总结了要点。我是这样想的。当您拥有可以执行多项操作的方法时,您是在增加复杂性,而不是增加复杂性。

                  换句话说,包装在 try catch 中的方法有两种可能的结果。您有非异常结果和异常结果。当您处理大量方法时,这会以指数方式爆炸,无法理解。

                  指数地,因为如果每个方法以两种不同的方式分支,那么每次您调用另一个方法时,您都会对先前的潜在结果数量进行平方。当您调用了五种方法时,您至少有多达 256 种可能的结果。将此与在每种方法中都进行 try/catch 相比,您只有一条路可走。

                  这基本上就是我的看法。您可能会争辩说任何类型的分支都做同样的事情,但 try/catch 是一种特殊情况,因为应用程序的状态基本上是未定义的。

                  简而言之,try/catch 让代码更难理解。

                  【讨论】:

                    【解决方案15】:

                    除了上面的建议,我个人使用了一些try+catch+throw;原因如下:

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

                    【讨论】:

                      【解决方案16】:

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

                      1. 您可以在要处理异常的地方使用此块,或者简单地说,编写的代码块可能会引发异常。
                      2. 如果您想在使用后立即处理您的对象,您可以使用try-catch 块。

                      【讨论】:

                      • "如果您想在使用后立即处理您的对象,您可以使用 try-catch 块。"您是否打算这样做来提升 RAII/最小对象生命周期?如果是这样,那么,try/catch 完全独立/正交。如果你想在更小的范围内处理对象,你可以打开一个新的{ Block likeThis; /* <- that object is destroyed here -> */ } - 无需将其包装在try/catch 中,除非你确实需要catch 任何东西,当然。
                      • #2 - 在异常中处理对象(手动创建)对我来说似乎很奇怪,这在某些语言中无疑是有用的,但通常你在 try/finally 中执行它try/except 块”,而不是专门在 except 块本身 - 因为对象本身可能首先是异常的原因,因此会导致另一个异常并可能崩溃。
                      猜你喜欢
                      • 2021-06-16
                      • 1970-01-01
                      • 1970-01-01
                      • 2012-02-18
                      • 2022-08-16
                      • 1970-01-01
                      • 2016-10-02
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多