把所有东西都包在try/catch块中,是否构成防御性编程?

38

我已经编程三年了。当我编写程序时,我习惯处理所有已知的异常并以友好的方式向用户发出警告。最近我看到了一些代码,几乎所有的方法都被包含在try/catch块中。作者说这是防御性编程的一部分。我想知道,这真的是防御性编程吗?你推荐将所有代码放在try块中吗?


6
不太确定。虽然标题很笼统,但问题实际上非常具体,涉及使用异常作为编程防御性方法,以及这样做的问题所在。 - Elie
12个回答

67

我的基本规则是:除非你能够解决导致异常的问题,否则不要捕获它,让它上升到可以处理它的级别。

根据我的经验,95% 的 catch 块要么忽略异常(catch {}),要么仅记录错误并重新抛出异常。后者可能看起来是正确的做法,但在实践中,当每个级别都这样做时,你最终只会得到五份相同错误消息的日志。通常这些应用程序在最高层有一个“忽略 catch”(因为“我们在所有较低层都有 try/catch”),导致应用程序非常缓慢,错过了很多异常,并且错误日志太长,没有人愿意去查看它。


9
没错。将所有代码都放在try/catch语句块中通常是一个经验不足的开发者的标志,他对异常并不了解。 - Jon Skeet
如果我们可以像扣分一样花费声望点数来进行mod+2,那就应该允许我们这样做。但我现在就在这里做了。 - Mark Brady
2
我认为有时候特别是在跨越模块或库的边界时,捕获异常、将其包装在不同类型的异常中(类似于Java的原因链式处理),然后重新抛出是一个好习惯。 - rmeador
这将是“部分处理”,它允许重新抛出异常,可能会有不同的异常。在我的书中,异常转换就是部分处理。 - Joris Timmermans

23

过度使用Try...Catch不是防御性编程,而是将尸体竖立起来。

在面对意外异常时,Try…Finally可以广泛用于恢复。只有当你预计到一个异常并知道如何处理它时,才应该使用Try..Catch。

有时我看到Try..Catch System.Exception的情况,其中catch块仅记录异常并重新抛出。这种方法至少存在以下三个问题:

  • 重新抛出假定存在未处理的异常,因此程序应该终止,因为它处于未知状态。但是catch会导致位于该Catch块下面的Finally块运行。在未定义的情况下,这些Finally块中的代码可能会使问题变得更糟。
  • 那些Finally块中的代码将改变程序状态。因此,任何记录都不能捕获最初抛出异常时的实际程序状态。而且,因为状态已经改变,调查会更加困难。
  • 它提供了糟糕的调试体验,因为调试器停在重新抛出语句上,而不是原始语句上。

14

不,这不是“防御性编程”。你的同事试图用一个好习惯的流行词来为他的坏习惯辩解。

他所做的应该被称为“掩盖问题”。这就像从方法调用中一致地忽略错误状态返回值。


1
我是否显得有些老派,因为无论errno是什么,我都会立刻想到printf打印“not a typewriter”? - Paul Tomblin

11

“防御性编程”的概念是指以一种方式编写代码,使其能够从错误情况中恢复或避免错误情况的发生。例如:

private String name;

public void setName(String name) {
}

当name == null时,您如何处理?抛出异常还是接受它?如果没有名称就没有对象是没有意义的,那么应该抛出异常。那么当name == ""时呢?

但是...后来您编写了一个编辑器。在设置UI时,您发现有些情况下用户可以决定将名称去掉,或者在用户编辑时名称可能变为空。

另一个例子:

public boolean isXXX (String s) {
}

在这里,防御策略通常是在 s == null 时返回 false(尽可能避免 NPE)。

或者:

public String getName() {
}

为了避免在调用代码中出现NPE,一个防御性程序员可能会在name==null时返回""。


9
如果你要处理随机异常,只需在应用程序的顶部处理它们,以便实现以下目的:
- 向用户呈现友好的消息 - 保存诊断信息
对于其他所有情况,你需要尽早捕获最直接、最具体的崩溃,否则异常处理就成为隐藏松散设计和代码的方法。
在大多数情况下,如果异常是可预测的,可以提前测试异常处理程序将捕获的条件。
总的来说,If...Else比Try...Catch更好。

我不同意。有些例外情况需要这样处理,但有一些例外情况需要更深入的处理 - 比如我的某些代码会抛出“ScheduleConflictException”,在更高层次上有一些代码来修改日程并再次尝试。 - Paul Tomblin
@Paul,“在大多数情况下”,我认为个人例外是一个例外。在这种情况下,您正在创建一个异常来处理更高级别的问题,我不认为这是cris5gd考虑的一般情况。 - he_the_great
1
@Stephan,代码中写着“将其添加到日程表”的部分比当前日程表是否可用的代码层级高很多,使用异常是最好的方式来将这个信息传递到所有层级。 - Paul Tomblin

8

随机捕获异常是不好的。那么该怎么办呢?

  • 忽略它们?太棒了。让我知道它们的工作原理。
  • 记录并继续运行?我想不是这样的。
  • 抛出另一个异常以使其崩溃?祝你调试好运。

捕获您可以实际处理有意义的异常是好的。这些情况很容易识别和维护。


第三个问题有什么难以调试的地方吗? - Bart van Heukelom
@Bart van Heukelom:不同的异常掩盖了原始异常,使日志变得混乱。日志消息是“SomeRandomException”。根本原因是ValueError。此外,原始回溯很容易在引发新异常的混乱中丢失。试试看。 - S.Lott
这就是Java的原因链技术。我认为其他编程语言也有这个功能(我知道PHP 5.3也有)。 - Bart van Heukelom
@Bart van Heukelom:Python具有链式异常。这可以帮助,但仅当有人正确使用时。除非他们转储了整个异常(这很少见),否则您仍然不知道它是链式异常的一部分。重写异常仍然很难调试。尝试在别人的代码中进行一段时间。真的很难。问题不是关于链式异常或类似的任何问题。问题是捕获异常过度或不必要,这是一个不同的主题。 - S.Lott

6

作为一个辅助翻译,我想说一下,每当我的同事在方法签名中使用“throws Exception”而不是列出方法真正抛出的异常类型时,我都想去开枪打他们的头。问题在于,经过一段时间后,你会发现有14个级别的调用都说“throws Exception”,因此重构以使它们声明它们真正抛出的异常是一个很大的练习。


由衷地同意!当然是用隐喻的方式表达,绝不是在鼓吹对任何人身体的任何部位进行射击。 - Bill Karwin
1
只需编写一个名为shootProgrammer(Programmer p)的方法,抛出OutOfAmmoException异常。 - Elie
2
@Bill - 我用卷起来的报纸打他们可以吗? - Paul Tomblin
让他们洗一下休息室水槽里滋生的所有咖啡杯,怎么样? - Bill Karwin

5
有一个问题叫做“过度处理”,捕获所有异常有点违背初衷。对于C++,catch(...)语句可以捕获所有异常,但是你无法处理该异常的内容,因为你不知道异常的类型(它可能是任何类型)。
你应该捕获你完全或部分处理得了的异常,并重新抛出部分异常。你不应该捕获任何你无法处理的异常,因为这只会掩盖以后可能(或者更确切地说,一定会)咬你的错误。

4
我建议不要这样做。当你知道可能会抛出的异常类型时,将代码放入try-catch块是可以接受的。正如你所说,它允许你优雅地恢复和/或向用户提示错误。然而,在你不知道可能发生的错误是什么的情况下,将所有代码放在这样的块中是使用异常来控制程序流程,这是一个大忌。
如果您编写良好结构化的代码,您将了解可能发生的每个异常,并且可以专门捕获这些异常。如果您不知道特定异常是如何被抛出的,请不要随便捕获它。当它发生时,您可以确定异常、导致异常的原因,然后捕获它。

3
我猜真正的答案是“这取决于情况”。如果try-catch块捕获非常通用的异常,那么我认为这与从未开车离开您的社区一样是一种防御性编程。在我看来,try-catch应该针对特定的异常进行定制。(再次说明,这只是我的个人意见),但我的防御性编程理念是你需要更少/更小的try-catch块,而不是更多/更大的块。您的代码应尽一切可能确保在第一次出现异常条件之前它可以完成所有操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接