多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过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异常处理的最好的文章。
它更倾向于未检查的异常,而不是已检查的异常,但这个选择的解释非常透彻,并基于强有力的论据。
我不想在这里引用太多的文章内容(最好是整体阅读),但它涵盖了这个线程中未检查异常倡导者的大部分论点。尤其是这个论点(似乎很受欢迎):
以异常在API层底部某处抛出的情况为例,因为没有人知道这个错误甚至可能发生,即使它是一种非常合理的错误类型,当调用代码抛出它时(例如FileNotFoundException而不是VogonsTrashingEarthExcept…)在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。
作者“回应”:
It is absolutely incorrect to assume that all runtime exceptions
should not be caught and allowed to propagate to the very "top" of the
application. (...) For every exceptional condition that is required to
be handled distinctly - by the system/business requirements -
programmers must decide where to catch it and what to do once the
condition is caught. This must be done strictly according to the
actual needs of the application, not based on a compiler alert. All
other errors must be allowed to freely propagate to the topmost
handler where they would be logged and a graceful (perhaps,
termination) action will be taken.
主要的思想或文章是:
当涉及到软件中的错误处理时,唯一安全且正确的假设是,存在的每个子程序或模块都可能发生故障!
因此,如果“没有人知道这个错误甚至可能发生”,那么这个项目就有问题了。像作者建议的那样,这种异常至少应该由最通用的异常处理程序来处理(例如,处理所有没有更特定的处理程序处理的异常)。
很遗憾,似乎没有多少人发现这篇伟大的文章:-(。我衷心建议每一个犹豫哪种方法更好的人花点时间阅读它。
SNR
首先,检查异常降低了代码的“信噪比”。Anders Hejlsberg也谈到了命令式编程和声明式编程,这是一个类似的概念。不管怎样,考虑下面的代码片段:
在Java中从非UI线程更新UI:
try {
// Run the update code on the Swing thread
SwingUtilities.invokeAndWait(() -> {
try {
// Update UI value from the file system data
FileUtility f = new FileUtility();
uiComponent.setValue(f.readSomething());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (InterruptedException ex) {
throw new IllegalStateException("Interrupted updating UI", ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException("Invocation target exception updating UI", ex);
}
在c#中从非UI线程更新UI:
private void UpdateValue()
{
// Ensure the update happens on the UI thread
if (InvokeRequired)
{
Invoke(new MethodInvoker(UpdateValue));
}
else
{
// Update UI value from the file system data
FileUtility f = new FileUtility();
uiComponent.Value = f.ReadSomething();
}
}
这对我来说清楚多了。当您开始在Swing中做越来越多的UI工作时,检查异常开始变得非常烦人和无用。
监狱打破
即使要实现最基本的实现,比如Java的List接口,作为契约式设计工具的受控异常也行不通。考虑一个由数据库、文件系统或任何其他抛出受控异常的实现支持的列表。唯一可能的实现是捕获已检查异常,并将其作为未检查异常重新抛出:
@Override
public void clear()
{
try
{
backingImplementation.clear();
}
catch (CheckedBackingImplException ex)
{
throw new IllegalStateException("Error clearing underlying list.", ex);
}
}
现在你要问所有这些代码的意义是什么?被检查的异常只是增加了噪音,异常被捕获但没有处理,契约式设计(就被检查的异常而言)已经失效。
结论
捕获异常与处理异常是不同的。
受控异常会给代码添加噪音。
没有它们,异常处理在c#中也能很好地工作。
我之前在博客上写过。