我想我读过的布鲁斯·埃克尔(Bruce Eckel)采访内容与我一样-一直困扰着我。实际上,争论是由受访者提出的(如果这确实是您正在谈论的职位)Anders Hejlsberg,.NET和C#的MS天才。
http://www.artima.com/intv/handcuffs.html
范虽然我是海斯伯格及其工作人员,但这种说法一直使我感到虚假。基本上可以归结为:
“受检查的异常是不好的,因为程序员只是通过始终捕获它们并将其关闭而滥用它们,这导致隐藏和忽略的问题,否则这些问题将被呈现给用户。”
通过“以其他方式呈现给用户的”我的意思是,如果你使用一个运行时异常懒惰的程序员会忽略它(而不是一个空的catch块捕获它),用户将看到它。
论点摘要的摘要是“程序员不会正确使用它们,而没有正确使用它们比没有它们更糟糕”。
这个论点有些道理,实际上,我怀疑高斯林不将运算符覆盖在Java中的动机来自一个类似的论点-它们使程序员感到困惑,因为它们经常被滥用。
但最后,我发现这是对海斯伯格的虚假论证,可能是后来提出的一种论证,用以解释这种缺乏而不是经过深思熟虑的决定。
我认为,尽管过度使用检查异常是一件坏事,并且容易导致用户草率处理,但正确使用它们会使API程序员为API客户端程序员带来巨大的好处。
现在,API程序员必须要小心,不要到处都抛出受检查的异常,否则它们只会惹恼客户程序员。(Exception) {}
当Hejlsberg警告时,非常懒惰的客户端程序员将求助于追赶者,所有利益都将丢失,随之而来的是地狱。但是在某些情况下,无法替代良好的检查异常。
对我而言,经典示例是文件打开API。语言历史中的每种编程语言(至少在文件系统上)在某个地方都有一个API,可让您打开文件。每个使用此API的客户端程序员都知道,他们必须处理自己尝试打开的文件不存在的情况。让我重申一下:每个使用此API的客户端程序员都应该知道他们必须处理这种情况。麻烦之处在于:API程序员可以帮助他们知道应该通过单独注释来处理它,还是可以确实要求客户处理它。
在C语言中,成语类似于
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
其中fopen
通过返回0和C(愚蠢地)来指示失败,这使您可以将0视为布尔值,并且...基本上,您学习了这个习语,并且还可以。但是,如果您是菜鸟,却没有学习成语怎么办。然后,当然,您从
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的惰性程序员只是添加空的catch。发生这种情况时,即使对测试人员或客户端开发人员进行调试,非法的行值也不会很明显,而是会导致连锁错误,而这些错误很难确定其来源。阿丽亚娜火箭将在发射后炸毁。
好的,这就是问题所在:我说的是,检查异常FileNotFoundException
不仅是一件好事,而且还是API程序员工具箱中用于以对客户端程序员最有用的方式定义API的基本工具。但是,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 ...."
简而言之,如果您认为客户程序员不能以对用户有用的方式解释异常,那么您可能不应该使用检查异常。
这就是我的规则。人为地作了准备,毫无疑问会有例外(如果可以,请帮助我完善它们)。但是我的主要论点是,在某些情况下,诸如FileNotFoundException
API契约的一部分所检查的异常与参数类型一样重要和有用。因此,我们不应仅仅因为滥用而放弃它。
抱歉,这并不意味着要花那么长时间。让我最后提出两个建议:
答:API程序员:谨慎使用受检查的异常以保留其有用性。如有疑问,请使用未经检查的异常。
B:客户程序员:养成在开发初期就创建包装异常(使用Google封装)的习惯。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) */}