【发布时间】:2011-02-13 19:19:41
【问题描述】:
我一直认为,如果一个方法可以抛出异常,那么不使用有意义的 try 块来保护这个调用是鲁莽的。
我刚刚向this question 发布了“你应该总是包装可以抛出 try、catch 块的调用。”并被告知这是“非常糟糕的建议”——我想明白为什么。
【问题讨论】:
标签: exception language-agnostic try-catch
我一直认为,如果一个方法可以抛出异常,那么不使用有意义的 try 块来保护这个调用是鲁莽的。
我刚刚向this question 发布了“你应该总是包装可以抛出 try、catch 块的调用。”并被告知这是“非常糟糕的建议”——我想明白为什么。
【问题讨论】:
标签: exception language-agnostic try-catch
一个方法应该只在它能够以某种合理的方式处理异常时才捕获它。
否则,将其向上传递,希望调用堆栈更高的方法可以理解它。
正如其他人所指出的,在调用堆栈的最高级别有一个未处理的异常处理程序(带有日志记录)是一种很好的做法,以确保记录任何致命错误。
【讨论】:
try 块有成本(就生成的代码而言)。 Scott Meyers 的“More Effective C++”中有很好的讨论。
try 块在任何现代 C 编译器中都是免费的,该信息的日期为 Nick。我也不同意拥有顶级异常处理程序,因为您会丢失位置信息(指令失败的实际位置)。
terminate)。它更多的是一种安全机制。此外,try/catch 在没有任何异常的情况下或多或少是免费的。当有一个传播时,它每次被抛出和捕获都会消耗时间,因此只有重新抛出的 try/catch 链并不是没有成本的。
正如Mitch and others 所说,您不应该捕获您不打算以某种方式处理的异常。在设计应用程序时,您应该考虑应用程序将如何系统地处理异常。这通常会导致基于抽象的错误处理层——例如,您在数据访问代码中处理所有与 SQL 相关的错误,这样与域对象交互的应用程序部分就不会暴露于以下事实:是某个地方的数据库。
除了“随处可见”异味之外,还有一些您绝对要避免的相关代码异味。
"catch, log, rethrow":如果您想要基于范围的日志记录,则编写一个类,当堆栈因异常而展开时(阿拉std::uncaught_exception())。您需要做的就是在您感兴趣的范围内声明一个日志记录实例,瞧,您有日志记录并且没有不必要的try/catch 逻辑。
“catch, throw translate”:这通常指向一个抽象问题。除非您正在实施一个联合解决方案,将几个特定异常转换为一个更通用的异常,否则您可能有一个不必要的抽象层......不要说“我明天可能需要它”.
“抓、清理、再扔”:这是我最讨厌的事情之一。如果您看到很多这样的情况,那么您应该应用Resource Acquisition is Initialization 技术并将清理部分放在 janitor 对象实例的析构函数中。
我认为充斥着try/catch 块的代码是代码审查和重构的好目标。这表明要么异常处理没有得到很好的理解,要么代码已经变成了变形虫,急需重构。
【讨论】:
log4j.Logger 的Logger 类,它在每个日志行中都包含线程ID,并在异常处于活动状态时在析构函数中发出警告。
因为下一个问题是“我发现了一个异常,接下来我该怎么做?”你会怎么做?如果您什么都不做 - 那就是错误隐藏,程序可能“无法正常工作”而没有任何机会找到发生的事情。您需要了解捕获异常后您将执行的具体操作,并且只有在您知道时才捕获。
【讨论】:
您不需要用 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 级消息。
Herb Sutter 写了关于这个问题的文章 here。绝对值得一读。
预告片:
“编写异常安全的代码基本上就是在正确的地方编写‘try’和‘catch’。”讨论。
坦率地说,该声明反映了对异常安全的根本误解。异常只是错误报告的另一种形式,我们当然知道编写错误安全代码不仅仅是检查返回码和处理错误条件的位置。
实际上,异常安全性很少与编写“try”和“catch”有关,而且越少越好。另外,永远不要忘记异常安全会影响一段代码的设计。它绝不是事后才想到的,可以通过一些额外的 catch 语句进行改造,就像调味一样。
【讨论】:
如其他答案所述,只有在可以对其进行某种合理的错误处理时,才应捕获异常。
例如,在产生您的问题的the question 中,提问者询问忽略lexical_cast 从整数到字符串的异常是否安全。这样的演员阵容永远不应该失败。如果它确实失败了,那么程序中出现了严重错误。在那种情况下你能做些什么来恢复?最好让程序死掉,因为它处于无法信任的状态。所以不处理异常可能是最安全的做法。
【讨论】:
如果你总是在可以抛出异常的方法的调用者中立即处理异常,那么异常就变得毫无用处,你最好使用错误代码。
异常的全部意义在于它们不需要在调用链中的每个方法中处理。
【讨论】:
我听到的最好的建议是,您应该只在可以明智地对异常情况采取措施的地方捕获异常,并且“捕获、记录和释放”不是一个好策略(如果在某些情况下偶尔无法避免)图书馆)。
【讨论】:
我得到了挽救几个项目的“机会”,而高管们更换了整个开发团队,因为该应用程序有太多错误,而且用户厌倦了这些问题并到处乱跑。这些代码库都在应用程序级别进行了集中式错误处理,就像投票最多的答案所描述的那样。如果该答案是最佳实践,为什么它不起作用并允许以前的开发团队解决问题?也许有时它不起作用?上面的答案没有提到开发人员花费多长时间来解决单个问题。如果解决问题的时间是关键指标,那么使用 try..catch 块检测代码是一种更好的做法。
我的团队如何在不显着更改 UI 的情况下解决问题?很简单,每个方法都使用 try..catch 进行检测,并在故障点记录所有内容,方法名称、方法参数值与错误消息、错误消息、应用程序名称、日期、和版本。有了这些信息,开发人员可以对错误进行分析,以确定发生最多的异常!或者错误数量最多的命名空间。它还可以验证模块中发生的错误是否得到妥善处理,而不是由多种原因引起的。
这样做的另一个好处是开发人员可以在错误记录方法中设置一个断点,并且只需单击一个断点并单击“退出”调试按钮,他们就可以在完全访问失败的方法中到故障点的实际对象,可在即时窗口中方便地使用。它使调试变得非常容易,并允许将执行拖回方法的开头以复制问题以找到确切的行。集中式异常处理是否允许开发人员在 30 秒内复制异常?没有。
语句“一个方法应该只在它能够以某种合理的方式处理异常时才捕获它。”这意味着开发人员可以预测或将遇到在发布之前可能发生的每个错误。如果这是真正的顶级,则不需要应用程序异常处理程序,并且 Elastic Search 和 Logstash 将没有市场。
这种方法还可以让开发人员发现并修复生产中的间歇性问题!您想在生产环境中不使用调试器进行调试吗?还是您宁愿接听电话并收到来自心烦意乱的用户的电子邮件?这使您可以在其他人知道之前解决问题,而无需通过电子邮件、IM 或 Slack 获得支持,因为解决问题所需的一切都在那里。 95% 的问题永远不需要重现。
为了正常工作,它需要与可以捕获命名空间/模块、类名、方法、输入和错误消息并存储在数据库中的集中式日志记录相结合,以便可以汇总以突出显示哪个方法最失败,所以可以先修好。
有时开发人员会选择从 catch 块中将异常向上抛出堆栈,但这种方法比不抛出的普通代码慢 100 倍。使用日志记录捕获和释放是首选。
在 12 位开发人员历时 2 年开发的财富 500 强公司中,该技术用于快速稳定大多数用户每小时都会失败的应用。使用这 3000 个不同的异常在 4 个月内被识别、修复、测试和部署。这平均每 15 分钟修复一次,持续 4 个月。
我同意输入检测代码所需的所有内容并不有趣,我更喜欢不查看重复的代码,但从长远来看,为每个方法添加 4 行代码是值得的。
【讨论】:
我同意您问题的基本方向,即在最低级别处理尽可能多的异常。
一些现有的答案类似于“您不需要处理异常。其他人会在堆栈中完成它。”根据我的经验,这是一个不考虑当前开发的代码段的异常处理的糟糕借口,让异常处理其他人或以后的问题。
这个问题在分布式开发中急剧增加,您可能需要调用由同事实现的方法。然后你必须检查一个嵌套的方法调用链,找出他/她为什么会向你抛出一些异常,这在最深的嵌套方法中可能更容易处理。
【讨论】:
我的计算机科学教授曾经给我的建议是:“只有在无法使用标准方法处理错误时才使用 Try 和 Catch 块。”
例如,他告诉我们,如果程序在无法执行以下操作的地方遇到严重问题:
int f()
{
// Do stuff
if (condition == false)
return -1;
return 0;
}
int condition = f();
if (f != 0)
{
// handle error
}
那么你应该使用 try, catch 块。虽然您可以使用异常来处理此问题,但通常不建议这样做,因为异常在性能方面代价高昂。
【讨论】:
如果您想测试每个函数的结果,请使用返回码。
例外的目的是让您可以更少地测试结果。这个想法是从你更普通的代码中分离出异常(不寻常的、罕见的)条件。这使普通代码更简洁,但仍然能够处理那些异常情况。
在精心设计的代码中,更深层次的函数可能会抛出,而更高层次的函数可能会捕获。但关键是许多“介于两者之间”的函数将完全摆脱处理异常情况的负担。它们只需要“异常安全”,这并不意味着它们必须捕获。
【讨论】:
我想在这个讨论中补充一点,从 C++11 开始,它确实很有意义,只要每个 catch 阻止 rethrows 异常直到它可以/应该处理的点。这样可以生成回溯。因此,我认为之前的观点部分已经过时了。
std::nested_exception 和std::throw_with_nested
在 StackOverflow here 和 here 上描述了如何实现这一点。
由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息! 你也可以看看我的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"
【讨论】:
我觉得有必要添加另一个答案,尽管 Mike Wheat 的回答很好地总结了要点。我是这样想的。当您拥有可以执行多项操作的方法时,您是在增加复杂性,而不是增加复杂性。
换句话说,包装在 try catch 中的方法有两种可能的结果。您有非异常结果和异常结果。当您处理大量方法时,这会以指数方式爆炸,无法理解。
指数地,因为如果每个方法以两种不同的方式分支,那么每次您调用另一个方法时,您都会对先前的潜在结果数量进行平方。当您调用了五种方法时,您至少有多达 256 种可能的结果。将此与不在每种方法中都进行 try/catch 相比,您只有一条路可走。
这基本上就是我的看法。您可能会争辩说任何类型的分支都做同样的事情,但 try/catch 是一种特殊情况,因为应用程序的状态基本上是未定义的。
简而言之,try/catch 让代码更难理解。
【讨论】:
除了上面的建议,我个人使用了一些try+catch+throw;原因如下:
【讨论】:
您无需在try-catch 中隐藏代码的每一部分。 try-catch 块的主要用途是错误处理和程序中的错误/异常。 try-catch 的一些用法 -
try-catch 块。【讨论】:
try/catch 完全独立/正交。如果你想在更小的范围内处理对象,你可以打开一个新的{ Block likeThis; /* <- that object is destroyed here -> */ } - 无需将其包装在try/catch 中,除非你确实需要catch 任何东西,当然。