我为什么不在“尝试” - “捕获”中包裹每个块?

时间:2010-04-29 12:43:53

标签: java c++ exception exception-handling try-catch

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

我刚刚发布了'你应该总是将可以投入的调用,阻止块。'包装到this question,并被告知这是'非常糟糕的建议' - 我会喜欢理解为什么。

15 个答案:

答案 0 :(得分:325)

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

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

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

答案 1 :(得分:133)

正如Mitch and others所述,您不应该抓住您不打算以某种方式处理的例外情况。您应该考虑应用程序在设计时如何系统地处理异常。这通常会导致基于抽象的错误处理层 - 例如,您处理数据访问代码中的所有SQL相关错误,以便与域对象交互的应用程序部分不会暴露于那里是某个地方的DB。

除了“抓住所有地方的”气味之外,还有一些相关的代码气味你肯定要避免。

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

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

  3. “抓住,清理,重新抛出”:这是我的宠儿之一。如果您看到很多这样的内容,那么您应该应用Resource Acquisition is Initialization技术并将清理部分放在 janitor 对象实例的析构函数中。

  4. 我认为充满try / catch块的代码是代码审查和重构的良好目标。它表明任何一个异常处理都没有得到很好的理解,或者代码已成为一个amœba并且非常需要重构。

答案 2 :(得分:46)

因为下一个问题是“我遇到了异常,我接下来该怎么办?”你会怎么做?如果你什么都不做 - 这就是隐藏的错误,程序可能“无法正常工作”而没有任何机会找到发生的事情。你需要了解一旦你发现异常后你将会做什么,只有你知道才能捕获。

答案 3 :(得分:24)

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

  

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

     直言不讳地说,这一陈述反映了对异常安全的根本误解。异常只是另一种形式的错误报告,我们当然知道编写错误安全代码不仅仅是检查返回代码和处理错误条件的位置。

     

实际上,事实证明异常安全很少是写'尝试'和'捕获' - 而且越少越好。此外,永远不要忘记异常安全会影响一段代码的设计;它不仅仅是一种事后的想法,可以用一些额外的捕获声明进行改装,就好像用于调味一样。

答案 4 :(得分:24)

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

这很好,有三个原因:它很方便,因为你只有一个地方可以报告错误: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容器,智能指针以及在析构函数中释放资源的其他对象,因为对象总是在异常之前被破坏。

答案 5 :(得分:14)

如其他答案中所述,如果你能为它做一些合理的错误处理,你应该只捕获一个例外。

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

答案 6 :(得分:11)

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

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

答案 7 :(得分:9)

我听过的最好的建议是,你应该只在你可以明智地对异常情况做些什么的时候捕捉异常,并且“捕获,记录和释放”不是一个好的策略(如果偶尔不可避免的话)库)。

答案 8 :(得分:6)

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

现有的一些答案就像“你不需要处理异常。别人会在堆栈中做到这一点。”根据我的经验,对于当前开发的代码中的异常处理,错误的理由关于异常处理,使异常处理其他人或以后的问题。

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

答案 9 :(得分:5)

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

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

int f()
{
    // Do stuff

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

int condition = f();

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

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

答案 10 :(得分:2)

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

例外的目的是让您可以经常测试结果。我们的想法是将特殊(不寻常,罕见)的条件从更普通的代码中分离出来。这使普通代码更清晰,更简单 - 但仍然能够处理这些异常情况。

在精心设计的代码中,更深层的函数可能会抛出,更高级的函数可能会捕获但关键是,“介于两者之间”的许多功能将完全免除处理特殊情况的负担。他们只需要“例外安全”,这并不意味着他们必须抓住。

答案 11 :(得分:1)

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

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

答案 12 :(得分:1)

我想在此讨论中添加自C ++ 11 以来,只要每个catch阻止rethrow s,它确实很有意义。直到可以/应该处理的异常。通过这种方式可以生成回溯。因此,我认为之前的意见已经过时了。

使用std::nested_exceptionstd::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"

答案 13 :(得分:0)

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

换句话说,包装在try catch中的方法有两个可能的结果。您有非异常结果和异常结果。当您处理许多方法时,它会以指数级的速度爆炸,超出了理解范围。

以指数方式表示,因为如果每种方法以两种不同的方式分支,那么每次调用另一种方法时,您的结果就变成原来的数量。到您调用五种方法时,您至少可以获得256种可能的结果。与此相比,在每种方法中都进行尝试/捕获,您只能遵循一条路径。

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

因此,简而言之,try / catches使代码难以理解。

答案 14 :(得分:-2)

您无需在try-catch内覆盖代码的每个部分。 try-catch块的主要用途是错误处理并在程序中出现错误/异常。 try-catch -

的一些用法
  1. 您可以在要处理异常的位置使用此块,或者只是可以说编写的代码块可能会引发异常。
  2. 如果您想在使用后立即处理对象,可以使用{{1}}阻止。