针对检查异常的情况
多年来,我一直无法得到一个体面的答案,以下问题:为什么有些开发人员如此反对检查异常? 我曾经进行过多次对话,在博客上阅读,阅读布鲁斯·艾克尔(Bruce Eckel)所说的话(我看到的第一个人反对他们)。
我目前正在编写一些新的代码,并且非常小心地注意我如何处理异常。 我试图看到“我们不喜欢检查过的例外”观点的观点,但我仍然看不到它。
我所有的谈话都以相同的问题结束了,而且没有得到答复......让我设置它:
一般来说(从Java的设计),
我听到的一个共同论点是,如果发生异常,那么开发人员要做的就是退出程序。
我听到的另一个常见的观点是,检查异常使得重构代码变得更加困难。
对于“我要做的就是退出”的论点,我说即使你退出,你也需要显示一个合理的错误信息。 如果你只是在处理错误,那么当程序退出时,用户不会过于高兴,而没有明确指出原因。
对于“重构难以重构”的人群来说,这表明没有选择适当的抽象层次。 IOException不应该声明方法抛出IOException,而应该将IOException转换为更适合正在发生的异常。
我没有用catch(Exception)(或者在某些情况下catch(Throwable)来包装Main以确保程序可以优雅地退出 - 但我总是捕获我需要的特定异常。这样做可以让我,至少应显示适当的错误消息。
人们从不回答的问题是:
如果抛出RuntimeException子类而不是Exception子类,那么你怎么知道你应该捕获什么?
如果答案是catch Exception,那么你也像处理系统异常一样处理程序员错误。 这对我来说似乎是错误的。
如果你捕获Throwable,那么你正在以同样的方式处理系统异常和VM错误(等等)。 这对我来说似乎是错误的。
如果答案是你只捕获了你知道抛出的异常,那么你怎么知道抛出了什么? 当程序员X抛出一个新的异常并忘记捕捉它时会发生什么? 这对我来说似乎非常危险。
我会说一个显示堆栈跟踪的程序是错误的。 那些不喜欢检查异常的人不觉得这样吗?
所以,如果你不喜欢检查的异常,你可以解释为什么不能回答没有得到回答的问题吗?
编辑:我没有寻找什么时候使用任何模型的建议,我在寻找的是为什么人们从RuntimeException延伸,因为他们不喜欢从Exception扩展和/或为什么他们捕获一个异常,然后重新抛出一个RuntimeException而不是添加抛出他们的方法。 我想了解不喜欢检查异常的动机。
我想我读过你所做过的同样的布鲁斯艾克尔采访 - 而且这总是让我感到困扰。 事实上,这个观点是由受访者提出的(如果这确实是你正在讨论的文章)Anders Hejlsberg,MS和.NET和C#背后的天才。
http://www.artima.com/intv/handcuffs.html
范虽然我是海耶斯伯格和他的作品,但这种说法总是让我觉得自己是假的。 它基本上归结为:
“检查异常是不好的,因为程序员通过总是捕获它们并解雇它们来滥用它们,从而导致问题被隐藏并被忽略,否则将被呈现给用户。
通过“以其他方式呈现给用户”我的意思是,如果您使用运行时异常,懒惰的程序员将忽略它(与用空的catch块捕捉它),并且用户将看到它。
争论总结的总结是“程序员不能正确使用它们,不恰当地使用它们比不使用它们更糟糕”。
这个论点有一些道理,事实上,我怀疑Goslings在Java中不把操作符覆盖的动机来自于类似的争论 - 他们混淆了程序员,因为他们经常被滥用。
但最终,我认为它是Hejlsberg的一个虚假的论证,可能是一个事后的论证,用来解释缺乏而不是精心设计的决定。
我认为虽然检查异常的过度使用是一件坏事,并且往往会导致用户的粗暴处理,但正确使用它们可以让API程序员给API客户端程序员带来很大好处。
现在,API程序员必须小心,不要在整个地方抛出检查过的异常,否则他们会干扰客户端程序员。 非常懒惰的客户端程序员会诉诸catch (Exception) {}
因为Hejlsberg警告并且所有的好处都会丢失,地狱也会随之而来。 但在某些情况下,只有一个好的检查异常没有替代品。
对我来说,典型的例子是文件打开API。 语言历史上的每一种编程语言(至少在文件系统上)都有一个可以让你打开文件的API。 每个使用此API的客户端程序员都知道他们必须处理他们试图打开的文件不存在的情况。 让我改述一下:每个使用此API的客户端程序员都应该知道他们必须处理这种情况。 还有一个问题:API程序员是否可以帮助他们知道他们应该通过单独评论来处理它,或者他们是否可以坚持让客户端处理它。
在C中,这个习语就像是一样
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
fopen
通过返回0和C来表示失败(愚蠢地)让你把0当作一个布尔值,并且......基本上,你学习这个习语并且你没事。 但是如果你是一个noob而你没有学习这个成语呢。 那么,当然,你开始
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
并学习困难的方式。
请注意,我们仅在这里讨论强类型语言:对于强类型语言中的API有什么清楚的概念:这是一个功能(方法)的大杂烩,供您为每个语言使用明确定义的协议。
明确定义的协议通常由方法签名定义。 这里fopen要求你传递一个字符串(或者在C的情况下是char *)。 如果你给它别的东西,你会得到一个编译时错误。 你没有遵循协议 - 你没有正确使用API。
在一些(模糊的)语言中,返回类型也是协议的一部分。 如果你尝试在某些语言中调用fopen()
的等价物而不将它分配给一个变量,那么你也会得到一个编译时错误(你只能用void函数来实现)。
我试图做的一点是:在静态类型语言中,API程序员鼓励客户端正确使用API,以防止他们的客户端代码在出现明显错误时进行编译。
(在像Ruby这样的动态类型语言中,你可以传递任何东西,比如float,作为文件名 - 并且它会被编译。为什么如果你甚至不去控制方法参数,那么用户会检查异常。这里提出的参数只适用于静态类型语言。)
那么,检查异常呢?
那么这里有一个可用于打开文件的Java API。
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
看到这个问题? 以下是该API方法的签名:
public FileInputStream(String name)
throws FileNotFoundException
请注意, FileNotFoundException
是一个检查异常。
API程序员对你这样说:“你可以用这个构造函数来创建一个新的FileInputStream
a)必须以文件名作为字符串传递
b)必须接受在运行时可能找不到该文件的可能性“
就我而言,这就是整个问题。
问题的关键基本上就是“不受程序员控制的事情”。 我首先想到的是,他/她意味着API程序员无法控制的东西。 但事实上,正确使用时检查异常应该适用于客户端程序员和API程序员无法控制的东西。 我认为这是不滥用检查异常的关键。
我认为文件打开很好地说明了这一点。 API程序员知道你可能会给他们一个文件名,在API被调用的时候这个文件名不存在,并且他们将不能够返回你想要的东西,但是必须抛出一个异常。 他们也知道这种情况会经常发生,并且客户程序员可能会希望在编写调用时文件名是正确的,但是在运行时可能会因为无法控制的原因而出错。
所以API明确表示:在你打电话给我时,会出现这个文件不存在的情况,而且你可以更好地处理它。
反案情况会更清楚。 想象一下,我在写一个表格API。 我有一个包含此方法的API的表模型:
public RowData getRowData(int row)
现在作为一名API程序员,我知道有些情况下,某些客户端会在表格外传递一个负值或行值。 所以我可能会试图抛出一个检查异常并强制客户端处理它:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(我当然不会真的叫它“检查”。)
这是检查异常的不好使用。 客户端代码将充满调用来获取行数据,其中每一个都将不得不使用try / catch,以及为什么? 他们是否会向用户报告错误的行被寻找? 可能不会 - 因为无论围绕我的表视图的UI是什么,它都不应该让用户进入请求非法行的状态。 所以这是客户端程序员的一个缺陷。
API程序员仍然可以预测客户端将编写这样的错误,并且应该使用像IllegalArgumentException
这样的运行时异常来处理它。
在getRowData
检查异常的情况下,这显然会导致Hejlsberg的懒惰程序员简单地添加空捕获量。 当发生这种情况时,即使对测试人员或客户端开发人员进行调试,非法的行值也不会很明显,相反他们会导致难以找出问题根源的连锁错误。 阿里安娜火箭发射后将爆炸。
好吧,这里是问题所在:我在说,检查的异常FileNotFoundException
不仅仅是一件好事,而是API程序员工具箱中的一个重要工具,用于以客户端程序员最有用的方式定义API。 但CheckedInvalidRowNumberException
是一个很大的不便,导致编程不好,应该避免。 但是,如何区分这种差异。
我想这不是一门精确的科学,我认为这是潜在的,也许在某种程度上证明Hejlsberg的论点是正确的。 但是我并不乐意把宝宝扔出去,所以让我在这里提取一些规则来区分良好的检查异常和不良:
脱离客户的控制或关闭与打开:
只有在错误情况不受API和客户端程序员控制的情况下,才应使用检查异常。 这与系统如何打开或关闭有关。 在客户端程序员有控制权限的用户界面中,例如,在所有按钮,键盘命令等添加和删除表视图(封闭系统)的行时,如果它试图从数据库中提取数据不存在的行。 在任何数量的用户/应用程序可以添加和删除文件的文件操作系统(开放系统)中,可以想象的是,客户请求的文件在他们不知情的情况下被删除,所以他们应该被期望处理它。
无处不在:
检查异常不应该在客户端经常进行的API调用中使用。 我经常指的是来自客户端代码中的很多地方 - 不经常及时。 所以客户端代码并不倾向于尝试打开相同的文件,但我的表视图从不同的方法获取RowData
。 特别是,我会写很多类似的代码
if (model.getRowData().getCell(0).isEmpty())
每次都要在try / catch中包装会很痛苦。
通知用户:
在可以想象将有用的错误消息呈现给最终用户的情况下,应该使用检查异常。 这是“什么时候发生的?” 我在上面提出的问题。 它也涉及到项目1.因为你可以预测客户端-API系统之外的某些东西可能导致文件不在那里,所以你可以合理地告诉用户它:
"Error: could not find the file 'goodluckfindingthisfile'"
由于你的非法行号是由内部错误引起的,并且没有用户的错误,所以实际上没有你可以给他们提供的有用信息。 如果您的应用程序不让运行时异常落入控制台,它可能最终会给他们一些丑陋的消息,如:
"Internal error occured: IllegalArgumentException in ...."
简而言之,如果你不认为你的客户程序员可以以帮助用户的方式解释你的异常,那么你应该不会使用检查的异常。
所以那些是我的规则。 有些做作,并且无疑会有例外(如果您愿意的话,请帮助我完善它们)。 但我的主要观点是,有些情况像FileNotFoundException
,其中检查的异常与参数类型一样重要且有用的API合约的一部分。 所以我们不应该因为它被滥用而放弃它。
对不起,并不意味着要这么长时间和这么长时间。 让我完成两个建议:
答:API程序员:谨慎使用检查的异常来保持其有用性。 如有疑问,请使用未经检查的例外。
B:客户程序员:习惯于在开发早期创建一个包装的异常(谷歌它)。 JDK 1.4及更高版本在RuntimeException
中为此提供了一个构造函数,但您也可以轻松创建自己的构造函数。 这是构造函数:
public RuntimeException(Throwable cause)
然后养成习惯,只要你必须处理一个检查的异常,并且你感觉懒惰(或者你认为API程序员在使用检查的异常时过于热心),不要只是吞下异常,并重新抛出它。
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
把它放在IDE的一个小代码模板中,当你感觉很懒时使用它。 这样,如果您真的需要处理检查的异常,您将在运行时看到问题后被迫返回并处理它。 因为,相信我(和Anders Hejlsberg),你永远不会回到你的TODO
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
关于检查异常的事情是,通过对这个概念的通常理解,它们并不是真正的例外。 相反,它们是API备选返回值。
异常的整个想法是,在调用链的某个位置引发的错误可能会冒起来,并且可能会在代码的某处进一步处理,而不需要介入代码担心。 另一方面,检查异常要求投掷者和捕捉者之间的每一级代码都声明他们知道可以通过它们的所有形式的异常。 如果检查的异常仅仅是调用者必须检查的特殊返回值,那么在实践中这与实际上几乎没有什么不同。 例如[伪]:
public [int or IOException] writeToStream(OutputStream stream) {
[void or IOException] a= stream.write(mybytes);
if (a instanceof IOException)
return a;
return mybytes.length;
}
由于Java不能做替代返回值,或者简单的内联元组作为返回值,所以检查的异常是合理的响应。
问题在于很多代码(包括标准库的大部分代码)都会错误地检查异常情况,以便发现真正的异常情况,您可能很想抓住几个级别。 为什么IOException不是RuntimeException? 在其他语言中,我可以让IO异常发生,如果我什么都不做,我的应用程序将停止,我会得到一个方便的堆栈跟踪来查看。 这是可能发生的最好的事情。
也许有两种方法可以从整个写入到流的过程中捕获所有IOExceptions,放弃进程并跳转到错误报告代码中; 在Java中,如果不在每个调用级别添加'throws IOException',即使它们本身没有IO,也不能这样做。 这种方法不需要知道异常处理; 不得不为他们的签名添加例外情况:
然后有很多荒谬的图书馆例外,如:
try {
httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
throw new CanNeverHappenException("oh dear!");
}
当你不得不像这样荒谬地弄糟你的代码时,难怪检查异常会收到一堆仇恨,尽管真的这只是简单的糟糕的API设计。
另一个特殊的不良影响是控制反转,其中组件A向通用组件B提供回调。组件A希望能够让异常从其回调抛出回到它称为组件B的位置,但它不能因为这会改变由B修复的回调接口。只能通过将真正的异常包装在RuntimeException中来完成它,而RuntimeException还有更多异常处理样板可供编写。
在Java中实现的检查异常及其标准库意味着样板,样板,样板。 用一种已经很冗长的语言来说,这不是一场胜利。
与其重新检查所有(许多)针对检查异常的原因,我只会选择一个。 我已经失去了我写这段代码的次数:
try {
// do stuff
} catch (AnnoyingcheckedException e) {
throw new RuntimeException(e);
}
99%的时间我无法做任何事情。 最后块进行任何必要的清理(或至少他们应该)。
我也失去了我见过的次数:
try {
// do stuff
} catch (AnnoyingCheckedException e) {
// do nothing
}
为什么? 因为有人不得不面对它,而且很懒惰。 它错了吗? 当然。 它发生了吗? 绝对。 如果这是一个未经检查的例外呢? 该应用程序会死掉(这比吞咽异常更好)。
然后我们使用异常作为流控制形式的代码,如java.text.Format。 Bzzzt。 错误。 将“abc”放入表单上的数字字段的用户并不例外。
好吧,我想这是三个原因。
链接地址: http://www.djcxy.com/p/25877.html上一篇: The case against checked exceptions
下一篇: How to catch all exceptions as soon as they occur by just one rescue statement