多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过Bruce Eckel说的话(我看到的第一个站出来反对他们的人)。

我目前正在编写一些新代码,并非常注意如何处理异常。我试图了解那些“我们不喜欢受控异常”的人的观点,但我仍然看不出来。

我的每一次谈话都以同样的问题结束。让我把它建立起来:

一般来说(从Java的设计方式来看),

Error is for things that should never be caught (VM has a peanut allergy and someone dropped a jar of peanuts on it) RuntimeException is for things that the programmer did wrong (programmer walked off the end of an array) Exception (except RuntimeException) is for things that are out of the programmer's control (disk fills up while writing to the file system, file handle limit for the process has been reached and you cannot open any more files) Throwable is simply the parent of all of the exception types.

我听到的一个常见的说法是,如果发生了异常,那么所有开发人员要做的就是退出程序。

我听到的另一个常见论点是受控异常使得重构代码更加困难。

对于“我要做的就是退出”的参数,我说即使你退出了,你也需要显示一个合理的错误消息。如果你只是在处理错误上押注,那么当程序在没有明确说明原因的情况下退出时,你的用户不会太高兴。

对于“它使重构变得困难”的人群来说,这表明没有选择适当的抽象级别。与其声明方法抛出IOException,不如将IOException转换为更适合当前情况的异常。

我对用catch(Exception)(或在某些情况下用catch(Throwable)包装Main没有问题,以确保程序可以优雅地退出-但我总是捕获我需要的特定异常。这样做至少可以显示适当的错误消息。

人们从来不会回答的问题是:

如果抛出RuntimeException 子类代替异常 子类,你怎么知道 你应该去抓?

如果答案是catch Exception,那么您也在以与系统异常相同的方式处理程序员错误。在我看来这是不对的。

如果你捕获Throwable,那么你是在以同样的方式处理系统异常和虚拟机错误(等等)。在我看来这是不对的。

如果答案是您只捕获您知道抛出的异常,那么您如何知道抛出了哪些异常呢?当程序员X抛出一个新的异常而忘记捕获它时会发生什么?这对我来说似乎很危险。

我认为显示堆栈跟踪的程序是错误的。不喜欢受控异常的人不会有这种感觉吗?

所以,如果你不喜欢受控异常,你能解释一下为什么不,并回答没有得到回答的问题吗?

我不是在寻求什么时候使用这两种模型的建议,我想知道的是为什么人们从RuntimeException扩展,因为他们不喜欢从Exception扩展,和/或为什么他们捕获一个异常然后重新抛出一个RuntimeException,而不是将抛出添加到他们的方法中。我想了解不喜欢受控异常的动机。


当前回答

好吧,这不是关于显示堆栈跟踪或无声崩溃。它是关于在层与层之间沟通错误的能力。

The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).

实际上,我现在在一个应用程序中处理这种情况。我们将异常重新打包为AppSpecificException。这使得签名非常干净,我们不必担心在签名中爆炸。

当然,现在我们需要在更高的级别上专门化错误处理,实现重试逻辑等等。所有的东西都是AppSpecificException,所以我们不能说“如果一个IOException被抛出,重试”或“如果ClassNotFound被抛出,完全中止”。我们没有可靠的方法来获得真正的异常,因为当它们在我们的代码和第三方代码之间传递时,东西会一次又一次地重新打包。

这就是为什么我非常喜欢python中的异常处理。你只能捕获你想要和/或能处理的东西。其他所有东西都冒出来了,就好像你自己重新扔了它一样(不管怎样你已经这么做了)。

我一次又一次地发现,在我提到的整个项目中,异常处理分为3类:

Catch and handle a specific exception. This is to implement retry logic, for example. Catch and rethrow other exceptions. All that happens here is usually logging, and its usually a trite message like "Unable to open $filename". These are errors you can't do anything about; only a higher levels knows enough to handle it. Catch everything and display an error message. This is usually at the very root of a dispatcher, and all it does it make sure it can communicate the error to the caller via a non-Exception mechanism (popup dialogue, marshaling an RPC Error object, etc).

其他回答

的确,受控异常一方面增加了程序的健壮性和正确性(你被迫对接口做出正确的声明——方法抛出的异常基本上是一种特殊的返回类型)。另一方面,你面临的问题是,由于异常“冒出来”,当你改变一个方法抛出的异常时,你经常需要改变大量的方法(所有的调用者,以及调用者的调用者,等等)。

Java中的受控异常不能解决后一个问题;c#和VB。我们把孩子和洗澡水一起倒掉。

OOPSLA 2005论文(或相关技术报告)中描述了一种折衷的好方法。

简而言之,它允许你说:方法g(x)像f(x)一样抛出,这意味着g抛出f抛出的所有异常。瞧,没有级联更改问题的受控异常。

虽然这是一篇学术论文,但我还是鼓励你阅读(部分)它,因为它很好地解释了受控异常的优点和缺点。

Artima发表了一篇对。net架构师之一Anders Hejlsberg的采访,其中尖锐地讨论了反对受控异常的论点。品性差的人:

The throws clause, at least the way it's implemented in Java, doesn't necessarily force you to handle the exceptions, but if you don't handle them, it forces you to acknowledge precisely which exceptions might pass through. It requires you to either catch declared exceptions or put them in your own throws clause. To work around this requirement, people do ridiculous things. For example, they decorate every method with, "throws Exception." That just completely defeats the feature, and you just made the programmer write more gobbledy gunk. That doesn't help anybody.

In my opinion the checked exceptions are a very fine concept. Unfortunately the most programmers who have worked together we me have another opinion so that the projects have a lot of wrong used exception handling. I have seen that the most programmers create one (only one) exception class, a subclass of RuntimeException. That contains a message, sometimes a multi language key. I have no chance to argument against this. I have the impression that I talk to a wall when I explain them what anti patterns are, what a contracts of a methods are... I'm a little bit disappointed.

但是今天,对所有事情都有一个通用运行时异常的概念显然是一种反模式。他们使用它来检查用户输入。该异常被抛出,以便用户对话框可以从中产生错误消息。但并不是每个方法的调用者都是对话!通过抛出运行时异常,方法的契约被更改但没有声明,因为它不是一个受控异常。

希望他们今天学到了一些东西,并将在另一个地方进行检查(这是有用和必要的)。只使用受控异常不能解决问题,但受控异常会向程序员发出信号,表明他实现了错误的东西。

试图解决这个尚未解决的问题:

如果抛出RuntimeException子类而不是Exception子类,那么您如何知道应该捕获什么?

恕我直言,这个问题包含似是而非的推理。仅仅因为API告诉你它抛出了什么并不意味着你在所有情况下都以相同的方式处理它。 换句话说,您需要捕获的异常取决于您使用抛出异常的组件的上下文。

例如:

如果我正在为数据库编写连接测试程序,或者编写检查用户输入XPath的有效性的程序,那么我可能希望捕获并报告操作抛出的所有已检查和未检查的异常。

然而,如果我正在编写一个处理引擎,我可能会以与NPE相同的方式处理XPathException (checked):我将让它运行到工作线程的顶部,跳过该批处理的其余部分,记录问题(或将其发送给支持部门进行诊断),并为用户联系支持人员留下反馈。

Anders在软件工程电台的第97集中谈到了受控异常的陷阱,以及他为什么把它们排除在c#之外。