为什么程序员有时会悄悄地吞噬异常?

46

我知道这是不好的做法,但我曾经在一个好的程序员写的代码中看到过吞掉异常。所以我想知道这种坏习惯是否有至少一个积极的点。

换句话说,这是不好的,但为什么好的程序员在极少情况下会使用它呢?

try
{
    //Some code
}
catch(Exception){}

72
一位优秀的程序员会添加注释来说明他使用它的原因。没有注释就吞下异常意味着他不是一个好的程序员。 - Sjoerd
34
@Sjoerd:不加注释地吞掉异常意味着他犯了错误。这并不特别说明他是否是一个优秀的程序员。谁知道,上下文可能会让含义变得明显。 - Adam Robinson
2
在试图捕获异常时,我意识到我至少应该加入一条注释,我不能不这样做。:( - James Black
3
至少应该将其写入输出窗口 Debug.WriteLine(ex.message); - fishhead
18
我通常对它们进行评论://哦,好吧... - leppie
显示剩余3条评论
16个回答

50

查看我的代码时,我发现在我的日志记录代码中有一个地方,在尝试写入文件和写入事件日志失败后,它会将错误吞噬掉,因为没有其他地方可以报告它。这是其中的一个例子:并没有很多这样的情况。


7
最糟糕的情况-喜欢它。类似于“创建错误报告时出现错误”,但更糟?最好至少向用户发出警告。未能生成文件,未能生成错误报告,然后未向用户发出警告,这似乎是客户支持的噩梦。'~' - Justian Meyer
2
是的,如果它不是Windows服务,我肯定会考虑显示一些内容。可以说,在某些情况下,如果无法记录日志,则应立即终止应用程序。然而,这并不是这种情况。 - Steven Sudit
6
@Shadow: 那会是一个极其不安全的假设。相反,我假设我是一个技能尚可的程序员,我的错误将会被优秀的程序员很快发现并纠正。 - Steven Sudit
1
如果用户错误配置了日志权限/路径,他们应该如何修复?他们现在不会知道。 - Tvde1
1
没有日志可能意味着很多事情。 - Tvde1
显示剩余5条评论

26

因为有时候程序员的知识比编译器更丰富。

例如,在某些虚构的语言中,除以0被视为一个错误:

try {
    x = 1 / (a*a + 1);
} catch (DivisionByZeroException ex) {
  // Cannot happen as a*a+1 is always positive
}

由于一些语言(如Java)要求捕获某些/许多/所有异常,编译器可能会抱怨,而程序员知道这是不可能的。有时,让编译器闭嘴的唯一方法就是明确地吞掉异常。

在编译器不抱怨的语言中,通常根本不会写空的catch语句块。

编辑:实际上,在catch语句块中,我会加入assert(false)。如果理论上不可能发生某些情况,但在实际中确实发生了,那么这是一个非常严重的问题;)

编辑2:Java仅需要捕获已检查的异常。稍微改了一下我的陈述。


1
Java不要求您捕获所有异常(只有已检查的异常),也不能捕获所有异常。 - helpermethod
5
(假设它不是一个复数。) - Steven Sudit
@Helper方法:即使您的代码中没有任何中断调用,仍需要捕获所有非RuntimeException(例如InterruptedException)。 - HoLyVieR
1
@Meriton:对于人来说,证明并不难:注意到 xx mod 4 = 0 或 1(而永远不是 2 或 3)。因此,xx + 1 mod 4 = 1 或 2(而永远不是 0)。这可能对编译器来说很困难,这正是我想要表达的。 - Sjoerd
1
这绝对不能成为借口。特别是当你认为或者知道它不可能发生的时候,吞掉异常是你能做的最糟糕的事情。如果你吞掉了一个你知道可能会发生的异常,你仍然可以尝试找出出了什么问题。但如果你吞掉了一个本来不可能发生却发生了的异常,你就完全失去了方向。而且既然这种情况是不可能发生的,针对它采取行动是完全免费的,因此吞掉它最多只是毫无意义而已。我总是像这样做:throw new AssertionError("This can’t happen", theException); - Ole V.V.
显示剩余6条评论

21

我认为一个好的程序员不会这样做而不解释。

try
{
    //Some code
}
catch(Exception e) {
    // Explanation of why the exception is being swallowed.
}

我很不可能无缘无故地默默吞下 Exception 基类。 我至少会尝试记录错误日志。


这是我今天在项目中偶然遇到的一个具体示例。 这是获取浏览器支持的 XMLHttpRequest 的 JavaScript 函数的一部分。

var XMLHttp = null;

if( window.XMLHttpRequest ) {
    try {
        XMLHttp = new XMLHttpRequest();
    } catch(e) {} // we've already checked that this is safe. 
}
else if( window.ActiveXObject ) {
...
}

由于try被包裹在检查要创建的对象类型的if...else if中,因此安全地吞咽异常。


在我看来,这是非常糟糕的。一个正确的代码应该要么不捕获异常,要么像Java中的catch(e) {throw new AssertionError(e);}那样处理。实际上,这两种方式没有区别,但是在这里吞掉异常是没有道理的。 - maaartinus
如果没有区别,那么就没有理由不这样做。而且,说某个东西非常糟糕并不是一种“谦虚”的观点。下次请自己保留意见。 - Bill the Lizard
我不是“谦虚”,而是“诚实”。恐怕我不会听从你的命令。如果你觉得我的评论很粗鲁,我道歉。 - maaartinus

16

我只能想到两种情况,我会忽略异常。

  1. 当关闭文件或数据库连接时。如果关闭时出错了,我该怎么办?我想我应该写出一些错误信息,但是如果我已经成功读取了数据,那么它似乎是多余的。而且这似乎是一个非常不可能发生的事件。

  2. 在错误处理中更合理:如果我试图写入日志文件失败了,我该在哪里写入错误,以表明我无法写入错误?在某些情况下,您可以尝试写入屏幕,但根据应用程序的不同,这可能是不可能的,例如没有屏幕的后台作业;或者仅仅让用户感到困惑,因为他们无论如何都不能对错误做出任何反应。

另一个帖子提到了一些情况,在这些情况下,您知道异常是不可能的,或者至少,要发生异常,编译器或操作环境必须存在重大问题,例如它没有正确地将两个数字相加,那么谁说异常是有意义的呢?

我完全同意,在这些罕见的情况下,您应该包含一条评论来解释为什么异常是无关紧要的或不可能发生的。但是,我会说,每当您编写潜在具有神秘色彩的代码时,都应该包含解释性评论。

旁注:为什么程序员经常包括像“x = x + 1; //将1添加到x”这样的注释,就像呆子一样,如果没有注释,我永远不会想到,但是又扔进去了没有解释的真正神秘的代码?

在原始帖子四年后的补充说明

当然,即使是好的程序员有时也会忽略异常的真正原因是:“我正在尝试让基本逻辑工作,我不确定如果出现此异常应该怎么办,我现在不想折腾它,我已经花费太多时间在正常的逻辑流程上了,但我以后会回来处理它。” 然后他们忘记了回来处理它。

顺便提一下,有些我认为是相当优秀的程序员表示的一个不好的理由是:"我不想向用户显示晦涩难懂的错误信息,那会让他们感到困惑。最好静默处理这个错误。" 这是一个糟糕的理由,因为:(a)你仍然可以将一些内容写入日志文件;(b)一个晦涩难懂的错误信息真的比让用户误以为操作成功但实际上并没有更糟糕吗?现在客户可能会认为他们的订单正在处理,但实际上并没有,或者他们认为救护车正在派往帮助他们正在痛苦呻吟的孩子,但实际上消息从未传递出去等等。即使在最微不足道的情况下,例如在论坛上发布一条平淡无奇的帖子未能成功,我也宁愿得到一个晦涩的提示信息,至少让我知道发生了什么问题,如果我关心的话我应该再试一次或打电话给某人,而不是说 "更新完成",而实际上并没有更新完成。


4
对于最后一段话点个赞。让我惊讶的是,甚至那些非常优秀的程序员也会忽略这样一个显而易见的事实。 - Crono
如果你因为电脑无法将两个数字相加而不得不吞下异常,那么你可能应该中止应用程序,关闭电脑,添加弹簧,并将电脑从桌子上弹出去。;-) - Brent Rittenhouse
如果计算机无法正确地将两个数字相加,那么你认为它能成功地格式化错误消息、打开到数据库的连接并将消息写入日志吗?@BrentRittenhouse - Jay
@Jay 什么?我……不会……我完全同意。为什么程序员经常包括像“x=x+1; // 将1添加到x”这样的注释,就像duh一样,如果没有注释,我永远不会明白,但是却加入没有解释的真正神秘的代码? 这真的是我最大的恶习之一。 - Brent Rittenhouse
@BrentRittenhouse 是的。我讨厌那些只是重复代码功能的注释。就像我的例子“将1加到x上”。我希望我能说这只是一个我编造的愚蠢极端例子,但我确实见过这样的注释。就好像我会说,“哦!!!原来加号是这么用的!我忘了。”至少给我一点提示,“x”是什么,为什么我们要在这里加1。 - Jay

9

有时候即使是优秀的程序员也会犯错。

要么这样,要么你对于一个优秀的程序员的看法与某些人不同(因为一个优秀的程序员会留下一些关于为什么异常被忽略而不是做出一些处理信息的理由)。


这并不总是一个错误,几乎总是如此,但并非完全总是。 - Steven Sudit
3
有时候这样的推理很简单:“我不在乎异常情况”。 - NotMe
@Chris:好的,但是当你吞掉所有异常时,你不知道你不关心的是什么。根据编程语言的不同,它可能是一些异步的东西,因此与你不介意失败的代码无关。 - Steven Sudit

8
因为老板明天要做演示,有些事情必须完成,而且没有什么比程序崩溃或显示错误信息更糟糕的了。但是当某些东西不起作用时,他总是可以“绕过去”的。
演示后,当然没有人会改进那些“快速修复”的部分。;-)

如果我明天才需要它,为什么我今天要提起它呢? - Jay

6
我认为这是由于Java的可检查异常。我个人讨厌它们,因为人们往往认为他们需要在任何地方编写异常处理代码。在我的看法中,在99%的代码中,你只需要将异常抛出到堆栈上而不是处理它。如果新创建的任何方法默认签名都有“throws Exception”,那么我会很高兴,这样我就不必处理异常了,除非我想要处理。
在两个地方,您只需要异常处理: 1. 对于资源清理,如关闭输入流、数据库连接或删除文件。 2. 在堆栈的顶部捕获异常并记录或向用户显示它。
有些地方你真的不需要在catch中做任何事情,比如处理Thread.sleep()的InterruptedException,这时你应该至少有一个注释来明确你真的不希望在那里发生任何事情。

4

是的,我经常在异常清理代码中使用它们,以便不掩盖原始异常。

例如,如果您的catch处理程序尝试回滚事务并在重新抛出异常之前关闭连接,则可能有几个原因导致回滚/关闭本身失败(因为由于原始异常已经处于某些故障状态)。因此,可能需要在try块中包装清理操作,并使用空catch处理程序,基本上表示“如果清理过程中出现问题,那么这有点可以接受,因为我们有更大的问题要报告”。


2
你可能想要记录清理失败的信息。 - Cameron MacFarland
是的,你可能可以,也可能不行。这取决于你的应用程序的性质。 - Mike Mooney
如果您在处理另一个失败时确实想要记录失败,那么情况就变得有些棘手了。 - Steven Sudit
Java现在有“抑制异常(suppressed exceptions)”的功能,可以更好地指示异常并非主要问题。 - Raedwald

2

当无法有效处理异常时,应确保它不会影响应用程序的正常运行,或者它可能代表已知但对整体影响很小的短暂条件。

一个简单的例子是日志记录。你不需要应用程序崩溃仅仅因为某些日志信息不能保存到你的后备存储中。

另一个例子可能是你有替代的处理方式,就像这样:

private Boolean SomeMethod() {
   Boolean isSuccessful = false;
   try {
      // call API function that throws one of several exceptions on failure.
      isSuccessful = true;
   } catch(Exception) { }

   return isSuccessful;
}

在上述示例中,你可能没有备用方案,但是你不希望它被筛选出来。对于仅在try块的最后一步设置返回值为成功状态的短方法而言,这是可以接受的。
不幸的是,许多调用会引发异常,而不仅仅是在结果中返回错误代码或false条件。我也看到过某些调用引发的异常比它们的文档建议的还要多,这也是我将在它们周围放置一个catch all的原因之一。

据说有很多破损的API。在我们的大学里,人们经常被告知在应该返回布尔值的地方抛出异常,这是非常不恰当的做法 - 而且他们不仅抛出异常,还抛出了无法从中恢复的已检查异常。 - helpermethod
@辅助方法:我们有几个关于异常的规则。首先,如果您可以处理它,则不是异常。其次,我们所有的方法都具有成功/失败代码作为结果。第三,在仅代表真实系统故障时才应抛出异常。例如,在Web应用程序中,数据库服务器离线。那是可以的。 - NotMe
2
为什么不尝试{ return true;} catch(Exception e) {return false;} ?? - Daniel Moura

2

我偶然看到了这个旧帖子,并惊讶地发现没有唯一合理的原因,就是为什么好的代码会简单地吞噬异常(无论是否有注释)。

当您使用库和代码时,这些代码位于您控制范围之外,经常会出现需要调用方法以获取可能存在或不存在的某些值,或执行在给定时间点可能不允许的操作的情况。如果您调用的方法在无法返回有效结果时引发异常而不提供控制流替代方案,则必须吞噬异常作为信号表明该值不可用或无法执行该操作。

好的程序员将尽一切努力避免使用异常处理来驱动控制流程,除非出现实际故障场景,这些故障场景很可能冒泡到用户作为错误。然而,好的程序员还广泛地重复使用库和框架以提高效率,并且了解重新编写库以提供更好的替代方案(例如TryGetValue模式)并不总是可行的选项。


如果这是您唯一能做的事情,而且没有损坏/不一致状态等副作用,那当然可以。但是您并不总是知道第三方代码中的副作用和错误。 - Alexey Frunze

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