我想我读了你做的布鲁斯·埃克尔(Bruce Eckel)的采访 - 它总是困扰着我。事实上,这个论点是由受访者(如果这确实是你正在谈论的帖子)Anders Hejlsberg提出的,他是.NET和C#背后的MS天才。
http://www.artima.com/intv/handcuffs.html
虽然我是海耶斯伯格和他的作品的粉丝,但这个论点总是让我感到虚假。它基本上可以归结为:
“检查的异常是不好的,因为程序员只是通过总是抓住它们并忽略它们来滥用它们,这会导致问题被隐藏和忽略,否则这些问题就会呈现给用户”。
通过“以其他方式呈现给用户”,我的意思是如果你使用运行时异常,懒惰的程序员就会忽略它(而不是用一个空的捕获块来捕获它),用户就会看到它。
这个论点的总结是“程序员不会正确地使用它们,没有正确使用它们比没有它们更糟糕”。
这个论点有一定的道理,事实上,我怀疑Goslings没有在Java中放置运算符覆盖的动机来自类似的论点 - 它们混淆了程序员,因为它们经常被滥用。
但最终,我发现这是海尔斯伯格的虚假论点,可能是为解释缺乏而创建的事后论点,而不是经过深思熟虑的决定。
我认为,虽然过度使用检查的异常是一件坏事,并且往往会导致用户草率地处理,但是正确使用它们可以使API程序员为API客户端程序员带来巨大好处。
现在,API程序员必须小心不要到处抛出经过检查的异常,否则它们只会惹恼客户端程序员。非常懒惰的客户端程序员将诉诸于Hejlsberg警告的捕获,所有的好处都将丧失,地狱将随之而来。但在某些情况下,一个好的检查异常是无可替代的。(Exception) {}
对我来说,经典的例子是文件开放API。语言历史上的每一种编程语言(至少在文件系统上)都有一个API,可以让你打开一个文件。每个使用此API的客户端程序员都知道,他们必须处理他们尝试打开的文件不存在的情况。让我再说一遍:每个使用此API的客户端程序员都应该知道他们必须处理这种情况。还有一个问题:API程序员能否帮助他们知道他们应该通过单独评论来处理它,或者他们是否真的坚持让客户处理它。
在C中,成语是这样的
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
其中,通过返回 0 和 C(愚蠢地)指示失败,允许您将 0 视为布尔值,并且...基本上,你学会了这个成语,你没事。但是,如果你是一个菜鸟,你没有学过这个成语呢?然后,当然,你从fopen
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
并以艰难的方式学习。
请注意,我们在这里只谈论强类型语言:在强类型语言中,API有一个清晰的概念:它是功能(方法)的大杂烩,你可以为每个语言使用一个明确定义的协议。
该明确定义的协议通常由方法签名定义。这里 fopen 要求你给它传递一个字符串(在 C 的情况下是一个字符*)。如果你给它一些别的东西,你会得到一个编译时错误。您没有遵循协议 - 您没有正确使用API。
在某些(晦涩难懂的)语言中,返回类型也是协议的一部分。如果您尝试在某些语言中调用等效项,而不将其分配给变量,则还会收到编译时错误(您只能使用void函数执行此操作)。fopen()
我想说的是:在静态类型语言中,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 程序员仍然可以预测客户端将编写此类 bug,并应使用运行时异常(如 .IllegalArgumentException
在 中有一个选中的例外,这显然是一个会导致 Hejlsberg 的懒惰程序员简单地添加空捕获的情况。发生这种情况时,即使对于测试人员或调试的客户端开发人员来说,非法行值也不会很明显,而是会导致难以确定其来源的连锁错误。阿里安娜火箭将在发射后爆炸。getRowData
好吧,这就是问题所在:我是说检查的异常不仅是一件好事,而且是API程序员工具箱中必不可少的工具,用于以对客户端程序员最有用的方式定义API。但这是一个很大的不便,导致糟糕的编程,应该避免。但是如何区分。FileNotFoundException
CheckedInvalidRowNumberException
我想这不是一门精确的科学,我想这是海尔斯伯格论点的基础,也许在某种程度上是合理的。但是我不高兴把婴儿和洗澡水一起扔在这里,所以请允许我在这里提取一些规则,以区分好的检查的例外和坏的例外:
-
超出客户端控制或已关闭与打开:
仅当错误情况不受 API 和客户端程序员的控制时,才应使用已检查的异常。这与系统的开放程度或封闭程度有关。在受约束的UI中,客户端程序员可以控制,例如,从表视图(封闭系统)添加和删除行的所有按钮,键盘命令等,如果它尝试从不存在的行中提取数据,则这是客户端编程错误。在基于文件的操作系统中,任意数量的用户/应用程序都可以添加和删除文件(开放系统),可以想象客户端请求的文件在他们不知情的情况下被删除,因此应该期望他们处理它。
-
到处存在:
已检查的异常不应用于客户端频繁进行的 API 调用。我经常是指从客户端代码中的很多地方 - 不经常及时。因此,客户端代码通常不会经常尝试打开同一文件,但我的表视图从不同的方法中到处都是。特别是,我将编写很多代码,例如RowData
if (model.getRowData().getCell(0).isEmpty())
每次都必须在尝试/捕获中包装将是痛苦的。
-
通知用户:
在可以想象向最终用户显示有用的错误消息的情况下,应使用已检查的异常。这就是我上面提出的“当它发生时你会怎么做?”的问题。它还涉及项目1。由于您可以预测客户端 API 系统外部的某些内容可能会导致文件不存在,因此您可以合理地告诉用户:
"Error: could not find the file 'goodluckfindingthisfile'"
由于您的非法行号是由内部错误引起的,并且不是用户的过错,因此您实际上无法向他们提供有用的信息。如果你的应用不让运行时异常传递到控制台,它最终可能会给他们一些丑陋的消息,比如:
"Internal error occured: IllegalArgumentException in ...."
简而言之,如果您认为您的客户端程序员无法以有助于用户的方式解释您的异常,那么您可能不应该使用已检查的异常。
所以这些是我的规则。有点人为的,毫无疑问会有例外(如果你愿意,请帮我完善它们)。但我的主要论点是,在某些情况下,选中的异常与参数类型一样重要且有用的API协定的一部分。因此,我们不应该仅仅因为它被滥用而放弃它。FileNotFoundException
抱歉,不是故意要让它变得如此漫长和华而不实。最后,我要提出两点建议:
答: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) */}