我一直认为,如果一个方法可以抛出异常,那么不使用有意义的 try 块来保护这个调用是鲁莽的。
我刚刚向 this question 发布了“你应该总是包装可以抛出 try、catch 块的调用。”并被告知这是“非常糟糕的建议”——我想了解原因。
一个方法应该只在它能够以某种合理的方式处理异常时才捕获它。
否则,将其向上传递,希望调用堆栈更高的方法可以理解它。
正如其他人所指出的,在调用堆栈的最高级别有一个未处理的异常处理程序(带有日志记录)以确保记录任何致命错误是一种很好的做法。
正如 Mitch and others 所述,您不应捕获您不打算以某种方式处理的异常。在设计应用程序时,您应该考虑应用程序将如何系统地处理异常。这通常会导致基于抽象的错误处理层——例如,您在数据访问代码中处理所有与 SQL 相关的错误,这样与域对象交互的应用程序部分就不会暴露于以下事实:是某个地方的数据库。
除了“随处可见”的气味之外,还有一些您绝对希望避免的相关代码气味。
“catch, log, rethrow”:如果您想要基于作用域的日志记录,则编写一个类,该类在堆栈因异常而展开时在其析构函数中发出日志语句(ala std::uncaught_exception())。您需要做的就是在您感兴趣的范围内声明一个日志记录实例,瞧,您有日志记录并且没有不必要的 try/catch 逻辑。 “catch, throw translate”:这通常指向一个抽象问题。除非您正在实施一个联合解决方案,将几个特定异常转换为一个更通用的异常,否则您可能有一个不必要的抽象层......并且不要说“我明天可能需要它”。 “捕捉,清理,再扔”:这是我最讨厌的事情之一。如果您看到很多这种情况,那么您应该应用资源获取是初始化技术并将清理部分放在看门人对象实例的析构函数中。
我认为充斥着 try
/catch
块的代码是代码审查和重构的好目标。这表明要么异常处理没有被很好地理解,要么代码已经变成了变形虫,并且迫切需要重构。
log4j.Logger
的 Logger
类,它在每个日志行中都包含线程 ID,并在异常处于活动状态时在析构函数中发出警告。
因为下一个问题是“我发现了一个异常,接下来我该怎么做?”你会怎么做?如果你什么都不做——那就是错误隐藏,程序可能“无法工作”而没有任何机会发现发生了什么。您需要了解捕获异常后您将做什么,并且只有在您知道时才捕获。
您不需要用 try-catch 覆盖 每个 块,因为 try-catch 仍然可以捕获在调用堆栈下方的函数中抛出的未处理异常。因此,与其让每个函数都有一个 try-catch,不如在应用程序的顶层逻辑中拥有一个。例如,可能有一个 SaveDocument()
顶级例程,它调用许多方法,这些方法又调用其他方法等。这些子方法不需要自己的 try-catch,因为如果它们抛出,它仍然会被 {1 } 的捕获。
这很好有三个原因:它很方便,因为您只有一个地方可以报告错误: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”有关——而且越少越好。另外,永远不要忘记异常安全会影响一段代码的设计。它绝不仅仅是事后的想法,可以通过一些额外的捕获语句进行改装,就像调味一样。
正如其他答案中所述,如果您可以对其进行某种合理的错误处理,您应该只捕获异常。
例如,在产生您的问题的 the question 中,提问者询问忽略 lexical_cast
从整数到字符串的异常是否安全。这样的演员阵容永远不会失败。如果它确实失败了,则程序中出现了严重错误。在那种情况下你能做些什么来恢复?最好让程序死掉,因为它处于无法信任的状态。所以不处理异常可能是最安全的做法。
如果你总是在可以抛出异常的方法的调用者中立即处理异常,那么异常就变得毫无用处,你最好使用错误代码。
异常的全部意义在于它们不需要在调用链中的每个方法中处理。
我听到的最好的建议是,您应该只在可以明智地对异常情况采取措施的地方捕获异常,并且“捕获、记录和释放”不是一个好策略(如果在库中偶尔无法避免)。
我得到了挽救几个项目的“机会”,高管们更换了整个开发团队,因为该应用程序有太多错误,用户厌倦了这些问题并跑来跑去。这些代码库都在应用程序级别进行了集中式错误处理,就像投票最多的答案所描述的那样。如果该答案是最佳实践,为什么它不起作用并允许以前的开发团队解决问题?也许有时它不起作用?上面的答案没有提到开发人员花费多长时间来解决单个问题。如果解决问题的时间是关键指标,那么使用 try..catch 块检测代码是一种更好的做法。
我的团队如何在不显着更改 UI 的情况下解决问题?很简单,每个方法都使用 try..catch 阻塞进行检测,并且在失败点记录所有内容,方法名称、方法参数值与错误消息、错误消息、应用程序名称、日期、和版本。有了这些信息,开发人员可以对错误进行分析,以确定发生最多的异常!或者错误数量最多的命名空间。它还可以验证模块中发生的错误是否得到了正确处理,而不是由多种原因引起的。
这样做的另一个好处是开发人员可以在错误记录方法中设置一个断点,并且通过一个断点和单击“step out”调试按钮,他们处于失败的方法中,可以完全访问实际故障点的对象,可在即时窗口中方便地使用。它使调试变得非常容易,并允许将执行拖回方法的开头以复制问题以找到确切的行。集中式异常处理是否允许开发人员在 30 秒内复制异常?不。
声明“一个方法应该只在它可以以某种合理的方式处理异常时才捕获它。”这意味着开发人员可以预测或将遇到在发布之前可能发生的每个错误。如果这是真正的顶级,则不需要应用程序异常处理程序,并且 Elastic Search 和 Logstash 将没有市场。
这种方法还可以让开发人员发现并修复生产中的间歇性问题!您想在生产环境中不使用调试器进行调试吗?还是您宁愿接听电话并收到来自心烦意乱的用户的电子邮件?这使您可以在其他人知道之前解决问题,而无需通过电子邮件、IM 或 Slack 获得支持,因为解决问题所需的一切都在那里。 95% 的问题永远不需要重现。
为了正常工作,它需要与可以捕获命名空间/模块、类名、方法、输入和错误消息并存储在数据库中的集中式日志记录相结合,以便可以聚合以突出显示哪个方法最失败,因此可以先修好了。
有时开发人员会选择从 catch 块中将异常向上抛出堆栈,但这种方法比不抛出的普通代码慢 100 倍。使用日志记录捕获和释放是首选。
在一家财富 500 强公司中,由 12 位开发人员历时 2 年开发,该技术用于快速稳定大多数用户每小时都会失败的应用程序。使用这 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 块。虽然您可以使用异常来处理此问题,但通常不建议这样做,因为异常在性能方面代价高昂。
如果要测试每个函数的结果,请使用返回码。
Exceptions 的目的是让您可以更少地测试结果。这个想法是从您更普通的代码中分离出异常(不寻常的、罕见的)条件。这使普通代码更干净、更简单——但仍然能够处理那些异常情况。
在设计良好的代码中,更深的函数可能会抛出,而更高的函数可能会捕获。但关键是,许多“介于两者之间”的函数将完全摆脱处理异常情况的负担。它们只需要“异常安全”,这并不意味着它们必须捕获。
我想在这个讨论中补充一点,从 C++11 开始,它确实很有意义,只要每个 catch
块 rethrow
都是异常,直到它出现可以/应该处理。这样可以生成回溯。因此,我认为以前的观点在一定程度上已经过时了。
使用 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+throw;原因如下:
在不同编码器的边界,我在自己编写的代码中使用try + catch + throw,然后将异常抛出给其他人编写的调用者,这让我有机会知道我的代码中发生了一些错误情况,并且这个地方更接近最初抛出异常的代码,越接近,越容易找到原因。在模块的边界,虽然不同的模块可能写我同一个人。学习+调试的目的,本例中我在C++中使用catch(...),在C#中使用catch(Exception ex),对于C++,标准库不会抛出太多异常,所以这种情况在C++中很少见。但是在 C# 中很常见,C# 有一个庞大的库和成熟的异常层次结构,C# 库代码抛出大量异常,理论上我(和你)应该知道你调用的函数的每个异常,并知道原因/案例为什么这些异常被抛出,并且知道如何优雅地处理它们(通过或捕获并就地处理)。不幸的是,实际上在我编写一行代码之前,很难了解潜在异常的所有信息。因此,当真正发生任何异常时,我会抓住所有内容并通过记录(在产品环境中)/断言对话框(在开发环境中)让我的代码大声说话。通过这种方式,我逐步添加异常处理代码。我知道它与好的建议相冲突,但实际上它对我有用,我不知道有什么更好的方法来解决这个问题。
您无需在 try-catch
中隐藏代码的每一部分。 try-catch
块的主要用途是错误处理和程序中的错误/异常。 try-catch
的一些用法 -
您可以在要处理异常的地方使用此块,或者您可以简单地说编写代码块可能会引发异常。如果您想在使用后立即处理您的对象,您可以使用 try-catch 块。
try
/catch
完全独立/正交。如果您想在更小的范围内处理对象,您只需打开一个新的 { Block likeThis; /* <- that object is destroyed here -> */ }
- 无需将其包装在 try
/catch
中,除非您确实需要 catch
任何东西,当然。
不定期副业成功案例分享
try
块存在成本(就生成的代码而言)。 Scott Meyers 的“More Effective C++”中有很好的讨论。try
块在任何现代 C 编译器中都是免费的,该信息的日期为 Nick。我也不同意拥有顶级异常处理程序,因为您会丢失位置信息(指令失败的实际位置)。try/catch
或多或少是免费的。当有一个传播时,它每次被抛出和捕获都会消耗时间,因此仅重新抛出的try/catch
链并非没有成本。