我想我读过和你一样的布鲁斯·埃克尔采访——它总是让我烦恼。事实上,这个论点是由 .NET 和 C# 背后的 MS 天才 Anders Hejlsberg(如果这确实是你正在谈论的帖子)提出的。
http://www.artima.com/intv/handcuffs.html
尽管我是 Hejlsberg 和他的作品的粉丝,但这个论点一直让我觉得是假的。它基本上归结为:
“检查的异常是不好的,因为程序员只是滥用它们,总是捕捉它们并解雇它们,这导致问题被隐藏和忽略,否则这些问题会呈现给用户”。
“以其他方式呈现给用户”是指如果您使用运行时异常,懒惰的程序员将忽略它(而不是用空的 catch 块捕获它)并且用户会看到它。
该论点的总结是“程序员不会正确使用它们,不正确使用它们比没有它们更糟糕”。
这个论点有一些道理,事实上,我怀疑 Gosling 不在 Java 中放置运算符覆盖的动机来自一个类似的论点——它们使程序员感到困惑,因为它们经常被滥用。
但最后,我发现这是 Hejlsberg 的一个虚假论点,并且可能是为了解释缺乏而创建的一个事后论点,而不是一个经过深思熟虑的决定。
我认为,虽然过度使用已检查异常是一件坏事,并且往往会导致用户处理草率,但是正确使用它们可以让 API 程序员给 API 客户端程序员带来很大的好处。
现在 API 程序员必须小心不要到处抛出已检查的异常,否则它们只会惹恼客户端程序员。Hejlsberg 警告说,非常懒惰的客户端程序员将求助于捕获(Exception) {}
,所有好处都将丢失,地狱将接踵而至。但是在某些情况下,一个好的检查异常是无可替代的。
对我来说,经典的例子是文件打开 API。语言历史上的每一种编程语言(至少在文件系统上)都有一个 API,可以让你打开文件。每个使用这个 API 的客户端程序员都知道他们必须处理他们试图打开的文件不存在的情况。让我换个说法:每个使用此 API 的客户端程序员都应该知道他们必须处理这种情况。还有一个问题:API 程序员是否可以帮助他们知道他们应该通过单独评论来处理它,或者他们确实可以坚持让客户处理它。
在 C 中,成语类似于
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
wherefopen
通过返回 0 表示失败,而 C(愚蠢地)让您将 0 视为布尔值,并且...基本上,您学习了这个习语就可以了。但是,如果您是菜鸟并且没有学习成语怎么办。然后,当然,你从
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
并努力学习。
请注意,我们在这里只讨论强类型语言:对于强类型语言中的 API 有一个清晰的概念:它是一个功能(方法)的大杂烩,供您使用,每个功能(方法)都有一个明确定义的协议。
该明确定义的协议通常由方法签名定义。此处 fopen 要求您向其传递一个字符串(或在 C 的情况下为 char*)。如果你给它其他东西,你会得到一个编译时错误。您没有遵守协议 - 您没有正确使用 API。
在某些(晦涩的)语言中,返回类型也是协议的一部分。如果您尝试fopen()
在某些语言中调用等价物而不将其分配给变量,您也会得到一个编译时错误(您只能使用 void 函数来做到这一点)。
我要说明的一点是:在静态类型语言中,API 程序员鼓励客户端正确使用 API,如果客户端代码出现任何明显错误,则阻止其编译。
(在动态类型语言中,比如 Ruby,你可以传递任何东西,比如浮点数,作为文件名——它会编译。如果你甚至不打算控制方法参数,为什么还要用检查异常来麻烦用户。此处提出的论点仅适用于静态类型语言。)
那么,检查异常呢?
好吧,这是您可以用来打开文件的 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和客户端程序员的控制范围时,才应使用已检查异常。这与系统的开放程度或封闭程度有关。在客户端程序员可以控制所有按钮、键盘命令等的受限UI 中,这些按钮、键盘命令等从表视图(封闭系统)中添加和删除行,如果它试图从一个不存在的行。在基于文件的操作系统中,任何数量的用户/应用程序都可以添加和删除文件(开放系统),可以想象客户端请求的文件已在他们不知情的情况下被删除,因此应该期望他们处理它.
无处不在:
不应在客户端频繁进行的 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:客户端程序员:养成在开发早期创建包装异常(google it)的习惯。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) */}