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

Kon*_*rad 424 c++ java exception-handling exception try-catch

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

我刚刚发布了' 你应该总是包装可以抛出try,catch块的调用.'对这个问题,并被告知这是'非常糟糕的建议' - 我想明白为什么.

Mit*_*eat 332

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

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

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

  • @Blindly:顶级异常处理程序不在那里处理异常,但实际上大声喊出有一个未处理的异常,给它的消息,并以优雅的方式结束程序(返回1而不是调用` terminate`).它更像是一种安全机制.此外,当没有任何异常时,`try/catch`或多或少都是免费的.当有一个传播时,每次投掷和捕获都会消耗时间,所以只有重新抛出的"try/catch"链才不会成本. (31认同)
  • 实际上`try`块在任何现代C编译器中都是免费的,该信息的日期为Nick.我也不同意有一个顶级异常处理程序,因为你丢失了位置信息(指令失败的实际位置). (28认同)
  • Kendall:如果异常到达顶级处理程序,则根据定义,您的应用程序处于未定义状态.虽然在某些特定情况下保留用户数据可能很有价值(想到Word的文档恢复)但程序不应覆盖任何文件或提交到数据库. (21认同)
  • 我不同意你应该总是崩溃未捕获的异常.现代软件设计是非常划分的,那么为什么要惩罚应用程序的其余部分(更重要的是,用户!)只是因为有一个错误?崩溃绝对是你想要做的最后一件事,至少尝试给用户一些小代码窗口,即使无法访问应用程序的其余部分,也可以让他们保存工作. (17认同)
  • 值得注意的是,"try"块有成本(就生成的代码而言).在Scott Meyers的"更有效的C++"中有一个很好的讨论. (11认同)
  • (但是像往常一样,在分析显示它成为问题之前不要过分担心它) (4认同)
  • 在所有编译器中,`try`块肯定不是免费的.具体来说,32位MSVC编译器仅为"try"块添加了相当大的开销(我已经看到测试将其固定在app的总运行时间的大约10-15%).其他一些编译器使用另一个异常实现,它避免了运行时开销,但在交换中使用了更多的内存.无论哪种方式,即使从未抛出异常,也会产生成本. (3认同)
  • @Kristopher:绝对同意顶级日志记录.记录错误后,不要让程序继续运行!该程序应该在此时使用核心转储. (2认同)
  • @Mitch:取决于你对"极端"的定义.:)这取决于编译器如何实现它,就像我说的,有几种策略取决于你是否要牺牲执行时间或内存,但是必须设置相当数量的管道以保持跟踪其中存在异常处理程序,在抛出异常时跳转到哪里,如何展开堆栈等等.当然,当实际抛出异常时,它会花费*extra*,但仅仅有一个`try`块也会花费一些成本. (2认同)
  • @LightnessRacesinOrbit,除其他外,我不能再编辑了.提出这一点究竟是什么意思?除了对自己感觉更好,我的意思是. (2认同)
  • @Blindy:我没有看到评论的年龄相关性.你写了它,它仍然是可见的.它可能会误导人们.我认为值得开始一个简短的对话,以验证它是无意的,所以如果不是那么我可以教育你,让你成为一个稍微有点知情的人.而且,如果是,那么至少其他访客将受益于迟来的纠正.对不起,如果你觉得我以某种方式"伤害"了你,但似乎没有必要进行人身攻击,不是吗? (2认同)

D.S*_*ley 135

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

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

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

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

  3. "抓住,清理,重新抛出":这是我的宠儿之一.如果您看到很多这样的内容,那么您应该应用资源获取是初始化技术,并将清理部分放在janitor对象实例的析构函数中.

我认为充满try/ catchblocks的代码是代码审查和重构的良好目标.它表明要么没有很好地理解异常处理,要么代码已成为amœba并且非常需要重构.

  • #1对我来说是新的.为此+1.另外,我想注意#2的一个常见例外,即如果你正在设计一个库,你通常会想要将内部异常转换为库接口指定的内容以减少耦合(这可能是你的意思通过"联合解决方案",但我不熟悉该术语). (6认同)
  • 基本上你说的是:http://www.parashift.com/c++-faq-lite/exceptions.html#faq-17.13 (3认同)

sha*_*oth 46

因为下一个问题是"我遇到了异常,我接下来该怎么做?" 你会怎么做?如果你什么都不做 - 这就是隐藏的错误,程序可能"无法正常工作"而没有任何机会找到发生的事情.一旦你发现异常,你需要了解你将会做什么,如果你知道的话,只能抓住.


Ash*_*ain 28

您不需要使用try-catches 覆盖每个块,因为try-catch仍然可以捕获调用堆栈中进一步调用的函数中未处理的异常.因此,不是让每个函数都有一个try-catch,你可以在应用程序的顶层逻辑中使用一个.例如,可能有一个SaveDocument()顶级例程,它调用许多调用其他方法的方法等.这些子方法不需要自己的try- SaveDocument()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;
}
Run Code Online (Sandbox Code Playgroud)

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

// these throw if failed, caught in SaveDocument's catch
SaveFirstSection();
SaveSecondSection();
SaveThirdSection();
Run Code Online (Sandbox Code Playgroud)

现在发生的事情要清楚得多.

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

  • 精彩的例子。是的,以逻辑单位(例如在某些“事务”操作(如加载/保存/等)周围)捕获尽可能高的值。看起来没有比带有重复的,冗余的“ try”-“ catch”块的代码更糟糕的了,这些块试图用略有不同的消息来标记某个错误的每个略有不同的排列,而实际上它们都应该以相同的结尾:事务或程序失败然后退出!如果发生了异常例外故障,我打赌大多数用户只想挽救自己可以解决的问题,或者至少可以独自一人解决,而不必处理与之相关的10条消息。 (2认同)

Tad*_*pec 27

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

"编写异常安全的代码基本上就是在正确的位置编写'try'和'catch'." 讨论.

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

事实上,事实证明异常安全很少是写'尝试'和'捕获' - 而且越少越好.此外,永远不要忘记异常安全会影响一段代码的设计; 它不仅仅是一个事后的想法,可以用一些额外的捕获声明进行改装,就好像用于调味一样.


Kri*_*son 15

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

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


sta*_*lue 12

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

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


Don*_*ows 9

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

  • 然而,捕获,记录和重新抛出可能是一个很好的策略. (2认同)
  • @KeithB:我认为这是第二好的策略.如果你能以另一种方式编写日志,那就更好了. (2认同)

Ban*_*zen 6

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

一些现有的答案就像"你不需要处理异常.其他人会在堆栈中做到这一点." 根据我的经验,这是一个错误的借口,不考虑当前开发的代码中的异常处理,使异常处理别人或以后的问题.

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


use*_*917 6

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

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

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

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

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

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

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

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

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

  • 包装每个块似乎有点矫枉过正。它很快使您的代码变得臃肿且难以阅读。从更高级别的异常记录堆栈跟踪会向您显示问题发生的位置,并且结合错误本身通常是足够的信息。我很好奇你在哪里发现这还不够。只是为了获得别人的经验。 (2认同)
  • “异常比正常代码慢 100 到 1000 倍,并且永远不应该重新抛出”——这种说法在大多数现代编译器和硬件上并不正确。 (2认同)

Mik*_*ley 5

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

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

int f()
{
    // Do stuff

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

int condition = f();

if (f != 0)
{
    // handle error
}
Run Code Online (Sandbox Code Playgroud)

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

  • 这是一种策略,但许多人建议**不要**从函数返回错误代码或失败/成功状态,而是使用异常.基于异常的错误处理通常比基于错误代码的代码更容易阅读.(请参阅AshleysBrain对此问题的回答.)另外,请记住,许多计算机科学教授在编写实际代码方面经验很少. (7认同)
  • @Kristopher:其他大缺点返回代码是,它是真正的容易忘记检查返回代码,而只是调用后是不是一定要处理这个问题的最佳场所. (3认同)