ChatGPT解决这个技术问题 Extra ChatGPT

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

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

我刚刚向 this question 发布了“你应该总是包装可以抛出 try、catch 块的调用。”并被告知这是“非常糟糕的建议”——我想了解原因。


M
Mitch Wheat

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

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

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


还值得注意的是,try 块存在成本(就生成的代码而言)。 Scott Meyers 的“More Effective C++”中有很好的讨论。
实际上 try 块在任何现代 C 编译器中都是免费的,该信息的日期为 Nick。我也不同意拥有顶级异常处理程序,因为您会丢失位置信息(指令失败的实际位置)。
@Blindly:顶级异常处理程序不是在那里处理异常,而是实际上大声喊出有一个未处理的异常,给出它的消息,并以优雅的方式结束程序(返回 1 而不是调用 { 1})。它更多的是一种安全机制。此外,如果没有任何异常,try/catch 或多或少是免费的。当有一个传播时,它每次被抛出和捕获都会消耗时间,因此仅重新抛出的 try/catch 链并非没有成本。
我不同意你应该总是在未捕获的异常上崩溃。现代软件设计是非常分割的,那么你为什么要仅仅因为一个错误就惩罚应用程序的其余部分(更重要的是用户!)?崩溃你绝对不想做的事情,至少尝试给用户一些小的代码窗口,即使无法访问应用程序的其余部分,也可以让他们节省工作。
Kendall:如果异常到达顶级处理程序,则根据定义,您的应用程序处于未定义状态。尽管在某些特定情况下保留用户数据可能很有价值(想到 Word 的文档恢复),但程序不应覆盖任何文件或提交到数据库。
C
Community

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

除了“随处可见”的气味之外,还有一些您绝对希望避免的相关代码气味。

“catch, log, rethrow”:如果您想要基于作用域的日志记录,则编写一个类,该类在堆栈因异常而展开时在其析构函数中发出日志语句(ala std::uncaught_exception())。您需要做的就是在您感兴趣的范围内声明一个日志记录实例,瞧,您有日志记录并且没有不必要的 try/catch 逻辑。 “catch, throw translate”:这通常指向一个抽象问题。除非您正在实施一个联合解决方案,将几个特定异常转换为一个更通用的异常,否则您可能有一个不必要的抽象层......并且不要说“我明天可能需要它”。 “捕捉,清理,再扔”:这是我最讨厌的事情之一。如果您看到很多这种情况,那么您应该应用资源获取是初始化技术并将清理部分放在看门人对象实例的析构函数中。

我认为充斥着 try/catch 块的代码是代码审查和重构的好目标。这表明要么异常处理没有被很好地理解,要么代码已经变成了变形虫,并且迫切需要重构。


#1对我来说是新的。为此+1。另外,我想指出 #2 的一个常见例外,即如果您经常设计一个库,您会希望将内部异常转换为库接口指定的内容以减少耦合(这可能是您的意思通过“联合解决方案”,但我不熟悉该术语)。
#2,它不是代码异味但有意义,可以通过将旧异常保留为嵌套异常来增强。
关于#1: std::uncaught_exception() 告诉您飞行中有一个未捕获的异常,但 AFAIK 只有一个 catch() 子句可以让您确定该异常实际上是什么。因此,虽然您可以记录由于未捕获的异常而退出范围的事实,但只有封闭的 try/catch 可以让您记录任何详细信息。正确的?
@Jeremy - 你是对的。我通常在处理异常时记录异常详细信息。跟踪中间帧非常有用。您通常还需要记录线程标识符或一些识别上下文以关联日志行。我使用了一个类似于 log4j.LoggerLogger 类,它在每个日志行中都包含线程 ID,并在异常处于活动状态时在析构函数中发出警告。
D
D.Shawley

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


M
Mitch Wheat

您不需要用 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 级消息。
只是想说这是我读过的最好的“早扔,晚抓”的解释之一:简明扼要,例子完美地说明了你的观点。谢谢!
C
Community

Herb Sutter 写了关于这个问题的文章 here。当然值得一读。
预告片:

“编写异常安全的代码基本上就是在正确的地方编写‘try’和‘catch’。”讨论。坦率地说,这种说法反映了对异常安全的根本误解。异常只是错误报告的另一种形式,我们当然知道编写错误安全代码不仅仅是检查返回代码和处理错误条件的位置。实际上,事实证明异常安全很少与编写“try”和“catch”有关——而且越少越好。另外,永远不要忘记异常安全会影响一段代码的设计。它绝不仅仅是事后的想法,可以通过一些额外的捕获语句进行改装,就像调味一样。


C
Community

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

例如,在产生您的问题的 the question 中,提问者询问忽略 lexical_cast 从整数到字符串的异常是否安全。这样的演员阵容永远不会失败。如果它确实失败了,则程序中出现了严重错误。在那种情况下你能做些什么来恢复?最好让程序死掉,因为它处于无法信任的状态。所以不处理异常可能是最安全的做法。


s
starblue

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

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


D
Donal Fellows

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


@KeithB:我认为这是次优策略。如果您能以另一种方式写入日志,那就更好了。
@KeithB:这是“图书馆总比没有好”的策略。 “捕捉、记录、妥善处理”在可能的情况下会更好。 (是的,我知道这并不总是可能的。)
u
user2502917

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

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

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

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

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

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

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

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

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


包装每个块似乎有点矫枉过正。它很快就会使您的代码变得臃肿且难以阅读。从更高级别的异常记录堆栈跟踪可以向您显示问题发生的位置以及与错误本身相结合通常足以继续进行的信息。我很好奇你在哪里发现这还不够。只是为了让我获得别人的经验。
“异常比普通代码慢 100 到 1000 倍,并且永远不应该被重新抛出”——这种说法在大多数现代编译器和硬件上都是不正确的。
这似乎有点矫枉过正,需要一些输入,但这是对异常执行分析以首先查找和修复最大错误(包括生产中的间歇性错误)的唯一方法。如果需要,catch 块会处理特定的错误,并有一行代码记录。
不,异常非常缓慢。另一种方法是返回代码、对象或变量。请参阅此堆栈溢出帖子...“异常至少比返回码慢 30,000 倍”stackoverflow.com/questions/891217/…
B
Bananeweizen

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

一些现有的答案类似于“您不需要处理异常。其他人会在堆栈中完成它。”以我的经验,这是一个不好的借口,不考虑当前开发的代码段的异常处理,使异常处理其他人或以后的问题。

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


M
Mike Bailey

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

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

int f()
{
    // Do stuff

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

int condition = f();

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

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


这是一种策略,但许多人建议不要从函数返回错误代码或失败/成功状态,而是使用异常。基于异常的错误处理通常比基于错误代码的代码更容易阅读。 (有关示例,请参阅 AshleysBrain 对此问题的回答。)另外,请始终记住,许多计算机科学教授几乎没有编写真正代码的经验。
-1 @Sagelika您的答案在于避免异常,因此不需要try-catch。
@Kristopher:返回代码的其他大缺点是很容易忘记检查返回代码,并且在调用之后不一定是处理问题的最佳位置。
嗯,这取决于,但在许多情况下(抛开那些真正不应该抛出的人),由于很多原因,异常优于返回代码。在大多数情况下,异常对性能有害的想法是一个很大的“[需要引用]
b
bluedog

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

Exceptions 的目的是让您可以更少地测试结果。这个想法是从您更普通的代码中分离出异常(不寻常的、罕见的)条件。这使普通代码更干净、更简单——但仍然能够处理那些异常情况。

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


G
GPMueller

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

使用 std::nested_exception 和 std::throw_with_nested

StackOverflow herehere 上描述了如何实现这一点。

由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息!您还可以查看我的 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"

u
user875234

尽管 Mike Wheat 的回答很好地总结了要点,但我觉得不得不添加另一个答案。我是这样想的。当您拥有执行多项操作的方法时,您是在增加复杂性,而不是增加复杂性。

换句话说,包装在 try catch 中的方法有两种可能的结果。您有非异常结果和异常结果。当您处理很多方法时,这会以指数方式爆炸,无法理解。

指数地,因为如果每个方法以两种不同的方式分支,那么每次你调用另一个方法时,你都会将先前的潜在结果数量平方。当您调用了五种方法时,您至少有多达 256 种可能的结果。将此与不在每种方法中都进行尝试/捕获进行比较,您只有一条路可走。

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

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


z
zhaorufei

除了上面的建议,我个人使用了一些try+catch+throw;原因如下:

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


A
Amit Kumawat

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

您可以在要处理异常的地方使用此块,或者您可以简单地说编写代码块可能会引发异常。如果您想在使用后立即处理您的对象,您可以使用 try-catch 块。


“如果您想在使用后立即处理您的对象,您可以使用 try-catch 块。”您是否打算这样做来提升 RAII/最小对象生命周期?如果是这样,那么 try/catch 完全独立/正交。如果您想在更小的范围内处理对象,您只需打开一个新的 { Block likeThis; /* <- that object is destroyed here -> */ } - 无需将其包装在 try/catch 中,除非您确实需要 catch 任何东西,当然。
#2 - 在异常中处理对象(手动创建)对我来说似乎很奇怪,这无疑在某些语言中很有用,但通常你在 try/finally “在 try/except 块内”执行它,而不是特别是在 except 块本身 - 因为对象本身可能首先是异常的原因,因此会导致另一个异常并可能导致崩溃。