为什么我不应该把每一块积木都用“try”-“catch”来包装?

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

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

91837 次浏览

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

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

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

因为下一个问题是“我已经捕获了一个异常,接下来我该做什么?”你会怎么做?如果你什么都不做——这是错误隐藏,程序可能“就是不能工作”,没有任何机会发现发生了什么。您需要了解捕获异常后要做什么,并且只有在知道的情况下才进行捕获。

我听到过的最好的建议是,您应该只在可以对异常条件采取措施的情况下捕获异常,而“捕获、记录和释放”并不是一个好策略(如果在库中偶尔不可避免的话)。

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

例如,在生成你的问题的这个问题中,提问者询问忽略lexical_cast从整数到字符串的异常是否安全。这样的阵容永远不会失败。如果它失败了,说明程序中出现了严重的错误。在这种情况下,你能做些什么来恢复呢?最好的方法可能是让程序死亡,因为它处于不可信任的状态。因此,不处理异常可能是最安全的做法。

Herb Sutter写了关于这个问题在这里。绝对值得一读。
摘要:< / p >

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

坦率地说,这种说法反映了对异常安全性的根本误解。异常只是错误报告的另一种形式,我们当然知道编写错误安全代码不仅仅是关于在哪里检查返回代码和处理错误条件。

实际上,异常安全很少涉及编写“try”和“catch”——而且越少越好。此外,永远不要忘记异常安全性会影响一段代码的设计;它从来不是一个事后的想法,可以用一些额外的捕捉语句进行改造,就像调味一样。

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

这很好,有三个原因:它很方便,因为你只有一个地方报告错误:SaveDocument() catch块。没有必要在所有子方法中重复这一点,而且这正是您想要的:在一个单一的位置为用户提供关于出错的有用诊断。

第二,每当抛出异常时,保存将被取消。对于每个try-catching子方法,如果抛出异常,则进入该方法的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容器、智能指针和其他在析构函数中释放其资源的对象,因为对象总是在异常之前被析构。

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

除了“到处抓一切”气味外,还有一些相关的代码气味是你一定要避免的。

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

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

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

我认为充斥着try/catch块的代码是代码审查和重构的好目标。它表明要么异常处理没有被很好地理解,要么代码已经变成了一个amœba,并且严重需要重构。

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

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

int f()
{
// Do stuff


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


int condition = f();


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

然后你应该使用try, catch块。虽然您可以使用异常来处理这个问题,但通常不建议这样做,因为异常会消耗大量性能。

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

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

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

一些现有的回答是这样的:“您不需要处理异常。别人会在上面做的。”根据我的经验,这是一个关于当前开发的代码段异常处理的不思考的糟糕借口,使异常处理其他人或以后的问题。

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

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

exception的目的是为了降低测试结果的频率。其思想是将异常(不寻常的,罕见的)条件从更普通的代码中分离出来。这使得普通代码更简洁,但仍然能够处理那些异常情况。

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

除了上面的建议,我个人使用尝试+抓+扔的方法;原因如下:

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

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

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

我得到了挽救几个项目的“机会”,高管们替换了整个开发团队,因为应用程序有太多错误,用户厌倦了这些问题和敷衍了事。这些代码库都在应用程序级别上有集中的错误处理,就像排名第一的答案所描述的那样。如果这个答案是最佳实践,为什么它不能让之前的开发团队解决问题呢?也许有时候它不起作用?上面的答案并没有提到开发者花了多长时间去修复单个问题。如果解决问题的时间是关键指标,那么使用try. catch块来检测代码是一个更好的实践。

我的团队是如何在不显著改变UI的情况下解决问题的?很简单,每个方法都被try..catch阻塞,所有的事情都记录在故障点,方法名,方法参数值连接到一个字符串中,与错误消息一起传递,错误消息,应用程序名称,日期和版本。有了这些信息,开发人员可以对错误进行分析,以确定发生最多的异常!或者错误数量最多的命名空间。它还可以验证模块中发生的错误是否得到了适当的处理,而不是由多种原因引起的。

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

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

这种方法还可以让开发人员发现并修复生产中的间歇性问题!是否希望在生产环境中不使用调试器进行调试?或者你宁愿接那些心烦意乱的用户的电话和邮件?这可以让你在其他人知道之前解决问题,而不必通过电子邮件、即时通讯或Slack寻求支持,因为解决问题所需的一切都在那里。95%的问题永远不需要被复制。

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

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

在一家财富500强公司中,该技术被用于快速稳定一款每小时都会出现故障的应用,该应用是由12名开发者历时2年开发的。使用这3000个不同的异常在4个月内被识别、修复、测试和部署。这平均每15分钟修复一次,持续4个月。

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

我想在这个讨论中补充一点,由于c++ 11,它确实很有意义,只要每个catchrethrows异常,直到它可以/应该被处理为止。这样可以生成反向跟踪。因此,我认为前面的观点在某种程度上已经过时了。

使用std::nested_exceptionstd::throw_with_nested

StackOverflow 在这里在这里中描述了如何实现这一点。

因为你可以用任何派生的异常类来做这件事,你可以在这样的回溯中添加很多信息! 你也可以看一下我的MWE在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"

尽管Mike Wheat的回答很好地总结了要点,但我还是觉得有必要再补充一个答案。我是这样想的。当你有方法做很多事情时,你是在增加复杂性,而不是增加它。

换句话说,封装在try catch中的方法有两种可能的结果。有非异常结果和异常结果。当你处理很多方法的时候这个指数级的爆炸超出了你的理解。

因为如果每个方法都以两种不同的方式分支,那么每次调用另一个方法时,你都是在对之前的潜在结果数进行平方。当你调用了5个方法时,你至少有256个可能的结果。与此相比,在每个方法中都执行try/catch,并且只有一条路径可以遵循。

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

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