为什么我不应该在“ try”-“ catch”中包装每个块?


430

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

我刚刚发布了“ 您应该总是包装可能会引发尝试,捕获块的调用。这个问题,并被告知这是“非常糟糕的建议”-我想了解原因。


4
如果我知道某个方法会引发异常,那么我将首先纠正它。这是因为我不知道代码在哪里以及为什么会抛出异常,因此我会在源头捕获它们(每种方法-一种通用的try-catch)。例如,一个团队为我提供了一个具有较早数据库的代码库。只有在SQL层中添加try catch之后,许多列才丢失并被捕获。其次,我可以记录方法名称和消息,以进行离线调试-否则我不知道它的起源。
Chakra

Answers:


340

方法只能以某种合理的方式处理它时才捕获异常。

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

正如其他人指出的那样,优良作法是在调用堆栈的最高级别上使用未处理的异常处理程序(带有日志记录),以确保记录所有致命错误。


12
还值得注意的是,代码try块存在成本(就生成的代码而言)。Scott Meyers的“更有效的C ++”中有很好的讨论。
尼克·迈耶

28
实际上try,任何现代C编译器中的块都是免费的,该信息标明了尼克的名字。我也不同意使用顶级异常处理程序,因为您会丢失位置信息(指令失败的实际位置)。
布林迪

31
@Blindly:没有顶级异常处理程序来处理异常,但实际上是大声喊出未处理的异常,给出其消息,并以一种优美的方式结束程序(返回1而不是调用terminate) 。这更像是一种安全机制。另外,try/catch在没有任何例外的情况下,或多或少都可以免费使用。当有一个传播时,它确实会在每次抛出和捕获时都消耗时间,因此try/catch,只有一次重新抛出并不是没有代价的。
Matthieu M.

17
我不同意您应该始终在发生未捕获的异常时崩溃。现代软件设计是非常区分的,那么为什么只因为一个错误而又要惩罚应用程序的其余部分(更重要的是用户!)?崩溃是您要做的最后一件事,至少要尝试给用户一些小的代码窗口,即使他们无法访问应用程序的其余部分,也可以使他们节省工作。
Kendall Helmstetter Gelner 2010年

21
肯德尔:如果异常到达顶级处理程序,则根据定义,您的应用程序处于未定义状态。尽管在某些特定情况下,保留用户数据可能有价值(想到了Word的文档恢复功能),该程序不应覆盖任何文件或提交数据库。
休·布拉克特

136

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

除了“随处捕获”气味之外,您还绝对要避免一些相关的代码气味。

  1. “捕获,日志,重新抛出”:如果要基于作用域的日志记录,则编写一个类,该类在堆栈由于异常而展开时会在其析构函数中发出log语句(ala std::uncaught_exception())。所有你需要做的就是申报的范围记录实例,你有兴趣和,瞧,你已经登录并没有不必要try/ catch逻辑。

  2. “捕获,抛出翻译”:这通常指向一个抽象问题。除非您要实现一种联合解决方案,在该解决方案中,您将几个特定的​​异常转换为一个更通用的异常,否则您可能会有不必要的抽象层…… 并且不要说“我明天可能需要它”

  3. “捕捉,清理,扔掉”:这是我的宠儿之一。如果您看到很多,则应应用“ 资源获取即初始化”技术,并将清除部分放置在管理员对象实例的析构函数中。

我认为,用try/ catch块填充的代码是代码查看和重构的良好目标。它表明,要么对异常处理的理解不充分,要么代码已成为难题,并且亟需进行重构。


6
#1对我来说是新的。+1。另外,我想指出#2的一个常见异常,即如果您在设计库时经常希望将内部异常转换为库接口指定的内容以减少耦合(这可能就是您的意思) (“联合解决方案”),但我对该术语不熟悉)。
rmeador


1
#2,它不是代码嗅觉,但有意义,可以通过将旧异常保留为嵌套异常来增强。
Deduplicator

1
关于#1:std :: uncaught_exception()告诉您有一个未捕获的异常正在运行,但是AFAIK只有catch()子句可让您确定该异常实际上是什么。因此,尽管您可以记录由于未捕获的异常而退出作用域的事实,但只有封闭的try / catch可以记录任何详细信息。正确?
杰里米(Jeremy)

@杰里米-你是对的。我通常在处理异常时记录异常详细信息。跟踪中间的帧非常有用。通常,您需要记录线程标识符或一些标识上下文,以关联日志行。我使用了一个Logger类似于log4j.Logger在每个日志行中包含线程ID 的类,并在异常活动时在析构函数中发出警告。
D.Shawley,

48

因为下一个问题是“我已经捕获到异常,下一步该怎么办?” 你会怎么做?如果您什么也不做-这是错误的隐藏,程序可能“无法运行”而没有机会发现发生了什么。您需要了解捕获异常后将要执行的操作,并且仅在知道的情况下捕获。


29

您不需要用try-catches 覆盖每个块,因为try-catch仍然可以捕获在调用堆栈之后的函数中引发的未处理异常。因此,除了让每个函数都具有try-catch之外,您还可以在应用程序的顶级逻辑中拥有一个。例如,可能有一个SaveDocument()顶层例程,该例程调用许多调用其他方法的方法,等等。这些子方法不需要它们自己的try-catches,因为如果它们抛出,它仍然会被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;
}

这是例外情况的写法:

// these throw if failed, caught in SaveDocument's catch
SaveFirstSection();
SaveSecondSection();
SaveThirdSection();

现在更清楚了发生了什么。

请注意,异常安全代码可以用其他方式来编写,比较棘手:如果抛出异常,您就不会泄漏任何内存。确保了解RAII,STL容器,智能指针和其他将其资源释放到析构函数中的对象,因为对象总是在异常之前被破坏。


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

我只是想说这是我读过的最好的“早起,晚抓”的解释之一:简明扼要,这些例子很好地说明了你的观点。谢谢!
corderazo00

27

赫伯萨特在这里写了这个问题。确实值得一读。
预告片:

“编写异常安全代码从根本上讲就是在正确的位置编写'try'和'catch'。” 讨论。

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

实际上,事实证明,异常安全性很少是关于编写“ try”和“ catch”的,而且越少越好。同样,永远不要忘记异常安全性会影响代码的设计。这绝不是事后的想法,可以通过添加一些额外的渔获量声明加以改进,就像调味一样。


15

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

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


12

如果始终在可以引发异常的方法的调用者中立即处理异常,则异常将变得无用,因此最好使用错误代码。

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


9

我听到的最好建议是,仅应在可以合理地对异常情况进行处理的地方捕获异常,并且“捕获,记录和释放”不是一个好的策略(如果偶尔在库中不可避免)。


2
@KeithB:我认为这是第二好的策略。最好以其他方式写日志。
David Thornley,2010年

1
@KeithB:这是一个“比在图书馆中做空更好”的策略。尽可能更好地“捕获,记录,正确处理”。(是的,我知道这并不总是可能的。)
Donal Fellows 2010年

6

我同意您问题的基本方向,以便在最低级别上处理尽可能多的例外。

现有的一些答案就像“您不需要处理异常。其他人将在堆栈中处理它”。以我的经验,这是一个不好的借口,不要思考在当前开发的代码中考虑异常处理,而让异常处理别人或以后的问题。

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


5

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

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

int f()
{
    // Do stuff

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

int condition = f();

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

然后,您应该使用try,catch块。尽管您可以使用异常来处理此问题,但通常不建议这样做,因为在性能上,异常是很昂贵的。


7
这是一种策略,但是许多人建议不要使用异常来从函数返回错误代码或失败/成功状态。与基于错误代码的代码相比,基于异常的错误处理通常更易于阅读。(有关示例,请参见AshleysBrain对这个问题的回答。)此外,请始终记住,许多计算机科学教授对编写真实代码的经验都很少。
克里斯托弗·约翰逊

1
-1 @Sagelika您的答案是避免出现异常,因此无需尝试捕获。
Vicente Botet Escriba,2010年

3
@Kristopher:返回码的其他主要缺点是,忘记返回码确实很容易,而且在调用之后不一定是处理问题的最佳位置。
David Thornley,2010年

嗯,这要看情况,但是在很多情况下(撇开真正不应该扔的人),由于许多原因,异常优于返回码。在大多数情况下,这个想法的例外是有害的性能是一个大醇” [来源请求]
underscore_d

3

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

例外的目的是使您可以经常测试结果。这个想法是要从您的普通代码中分离出异常(异常,稀有)条件。这使普通代码更简洁,更简单-但仍然能够处理那些特殊情况。

在设计良好的代码中,更深层的功能可能会抛出,而更高层的功能可能会捉住。但是关键是,“中间”的许多功能将完全没有处理特殊情况的负担。它们只必须是“异常安全的”,这并不意味着它们必须被捕获。


2

我想补充一下这一讨论,因为自C ++ 11以来,它确实很有意义,只要每个catchrethrow都是异常,直到可以/应该处理该点为止。这样可以生成回溯。因此,我认为先前的观点在某种程度上已经过时了。

使用std::nested_exceptionstd::throw_with_nested

它在此处以及此处如何实现的StackOverflow上进行描述。

由于您可以使用任何派生的异常类执行此操作,因此可以向此类回溯中添加很多信息!您也可以在GitHub上查看我的MWE,回溯显示如下:

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"

2

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

我的团队如何在不显着更改UI的情况下解决问题?很简单,每个方法都使用try..catch进行阻止,并在故障点记录所有内容,包括方法名称,方法参数值连接到字符串中,以及错误消息,错误消息,应用程序名称,日期,和版本。利用此信息,开发人员可以对错误进行分析,以找出最常发生的异常!或错误次数最多的名称空间。它还可以验证是否正确处理了模块中发生的错误,并且该错误不是由多种原因引起的。

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

语句“方法只能以某种明智的方式处理它时才捕获异常”。这意味着开发人员可以预测或将遇到发行前可能发生的所有错误。如果确实如此,那么就不需要应用程序异常处理程序,并且Elastic Search和logstash不会有市场。

这种方法还使开发人员可以发现并解决生产中的间歇性问题!您想在生产环境中不使用调试器进行调试吗?还是您更愿意接听电话并从烦恼的用户那里收到电子邮件?这使您可以在其他人不知道的情况下解决问题,而无需通过电子邮件,IM或Slack寻求支持,因为解决问题所需的一切就在那里。95%的问题都无需复制。

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

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

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

我同意键入检测代码所需的所有内容并不是一件好事,并且我宁愿不要查看重复的代码,但从长远来看,为每个方法添加4行代码是值得的。


1
包装每个块似乎是过分的。它迅速使您的代码肿且难以阅读。从更高级别的异常中记录一个堆栈跟踪信息,可以显示问题发生的位置,并且与错误本身结合在一起通常就足以说明问题。我很想知道您在哪里发现这还不够。这样我就可以获得别人的经验。
user441521

1
“异常比普通代码慢100到1000倍,并且永远不应重新抛出”-在大多数现代编译器和硬件上,这种说法是不正确的。
米奇·

这似乎有点过头,并且需要一点输入,但这是对异常执行分析以首先发现并修复最大错误(包括生产中的间歇性错误)的唯一方法。catch块根据需要处理特定的错误,并且具有记录的单行代码。
user2502917 18-10-25

不,例外情况非常缓慢。替代方法是返回码,对象或变量。请参阅此堆栈溢出文章...“异常至少比返回码慢30,000倍” stackoverflow.com/questions/891217/…–
user2502917

1

除了上述建议外,我个人还使用try + catch + throw;出于以下原因:

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

1

尽管迈克·惠特的答案很好地总结了要点,但我还是不得不添加另一个答案。我是这样想的。当您拥有执行多项任务的方法时,您正在增加复杂性,而不是相加。

换句话说,包装在try catch中的方法有两个可能的结果。您有非异常结果和异常结果。当您处理许多方法时,这将成倍地激增,超出了理解范围。

指数级地表示,因为如果每种方法以两种不同的方式分支,那么每次调用另一种方法时,您的结果就变成了以前的数量。到您调用五种方法时,您至少可以获得256种可能的结果。与之相比,不要在每种方法中都进行try / catch,则只有一条路径可以遵循。

基本上这就是我的看法。您可能会争辩说任何类型的分支都可以做相同的事情,但是try / catches是一种特例,因为应用程序的状态基本上变得不确定。

简而言之,尝试/捕获使代码难于理解。


-2

您无需掩盖其中的代码的每个部分try-catch。该try-catch块的主要用途是错误处理,并在程序中包含错误/异常。的一些使用try-catch-

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

1
“如果要在使用对象后立即处置它们,则可以使用try-catch块。” 您是否打算以此来提高RAII /最小对象寿命?如果是这样的话,try/ catch与此完全分离/正交。如果要在较小的范围内放置对象,则可以打开一个新对象{ Block likeThis; /* <- that object is destroyed here -> */ }-无需将其包装在try/中,catch除非您确实需要catch任何东西。
underscore_d

#2-在我看来,异常处理对象(手动创建的对象)似乎很奇怪,这无疑在某些语言中很有用,但是通常您可以在try / finally中“在try / except块内”进行操作,而不是特别是在except块本身中-因为对象本身首先可能是引起异常的原因,因此导致了另一个异常并可能导致崩溃。
TS TS
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.