多年来,我一直无法对以下问题得到一个像样的答案:为什么有些开发人员如此反对检查异常?我进行了无数次对话,阅读了博客上的内容,阅读了 Bruce Eckel 不得不说的话(我看到的第一个公开反对他们的人)。
我目前正在编写一些新代码并非常注意我如何处理异常。我试图了解“我们不喜欢检查异常”人群的观点,但我仍然看不到。
我的每一次谈话都以同样的问题没有得到回答而结束……让我来设置一下:
一般来说(从 Java 的设计方式来看),
Error
用于不应该被抓到的东西(VM 对花生过敏,有人在上面掉了一罐花生)RuntimeException
用于程序员做错的事情(程序员离开了数组的末尾)Exception
(除了RuntimeException
)用于程序员无法控制的事情(写入文件系统时磁盘已满,已达到进程的文件句柄限制,你不能打开更多文件)Throwable
只是所有异常类型的父级。
我听到的一个常见论点是,如果发生异常,那么开发人员要做的就是退出程序。
我听到的另一个常见论点是检查异常使重构代码变得更加困难。
对于“我要做的就是退出”的论点,我说即使你正在退出,你也需要显示一条合理的错误消息。如果您只是赌注处理错误,那么当程序在没有明确说明原因的情况下退出时,您的用户不会太高兴。
对于“重构变得困难”的人群来说,这表明没有选择适当的抽象级别。而不是声明一个方法抛出 IOException
, IOException
应该被转换成一个更适合正在发生的事情的异常。
我对用 catch(Exception)
包装 Main 没有问题(或者在某些情况下 catch(Throwable)
以确保程序可以正常退出 - 但我总是捕获我需要的特定异常。这样做至少可以让我显示一条适当的错误消息。
人们从不回答的问题是:
如果你抛出
RuntimeException
子类而不是Exception
子类那么你怎么知道你应该抓住什么?
如果答案是 catch Exception
那么您也在以与系统异常相同的方式处理程序员错误。这对我来说似乎是错误的。
如果您捕获 Throwable
那么您将以相同的方式处理系统异常和 VM 错误(等等)。这对我来说似乎是错误的。
如果答案是你只捕获你知道抛出的异常,那么你怎么知道抛出了哪些异常?当程序员 X 抛出一个新的异常并忘记捕获它时会发生什么?这对我来说似乎很危险。
我会说显示堆栈跟踪的程序是错误的。不喜欢检查异常的人不会有这种感觉吗?
所以,如果你不喜欢检查异常,你能解释为什么不喜欢并回答没有得到回答的问题吗?
我不是在寻找关于何时使用任何一种模型的建议,我在寻找的是 为什么 人们从 RuntimeException
扩展,因为他们不喜欢从 Exception
扩展和/或为什么他们捕获异常然后重新抛出 RuntimeException
而不是将抛出添加到他们的方法中。我想了解不喜欢检查异常的动机。
原文由 TofuBeer 发布,翻译遵循 CC BY-SA 4.0 许可协议
我想我读了和你一样的 Bruce Eckel 采访——它总是让我心烦意乱。事实上,这个论点是由受访者提出的(如果这确实是您正在谈论的帖子)Anders Hejlsberg,.NET 和 C# 背后的 MS 天才。
虽然我是 Hejlsberg 和他的作品的粉丝,但我一直觉得这个论点是假的。它基本上归结为:
通过 “以其他方式呈现给用户” ,我的意思是如果您使用运行时异常,懒惰的程序员将忽略它(而不是用空的 catch 块捕获它)并且用户会看到它。
争论的总结是 “程序员不会正确地使用它们,而不正确地使用它们比没有它们更糟糕” 。
这个论点有一定的道理,事实上,我怀疑 Gosling 不在 Java 中放置运算符覆盖的动机来自一个类似的论点——它们使程序员感到困惑,因为它们经常被滥用。
但最后,我发现这是 Hejlsberg 的虚假论点,可能是为了解释缺乏而不是经过深思熟虑的决定而创造的事后论点。
我会争辩说,虽然过度使用检查异常是一件坏事,而且往往会导致用户草率处理,但正确使用它们可以让 API 程序员为 API 客户端程序员带来很大好处。
现在 API 程序员必须小心不要到处抛出检查异常,否则它们只会惹恼客户程序员。正如 Hejlsberg 警告的那样,非常懒惰的客户端程序员将求助于 catch
(Exception) {}
,所有的好处都将丢失,地狱将接踵而至。但在某些情况下,一个好的检查异常是无可替代的。对我而言,经典示例是文件打开 API。语言历史上的每一种编程语言(至少在文件系统上)都有一个 API 可以让你打开一个文件。每个使用此 API 的客户端程序员都知道他们必须处理他们试图打开的文件不存在的情况。让我换句话说:每个使用此 API 的客户端程序员 都应该知道 他们必须处理这种情况。还有一个问题:API 程序员可以帮助他们知道他们应该通过单独评论来处理它,还是他们真的可以 坚持 让客户处理它。
在 C 中,成语是这样的
其中
fopen
通过返回 0 表示失败,而 C(愚蠢地)让您将 0 视为布尔值并且…基本上,您学习了这个习语就可以了。但是,如果您是菜鸟并且没有学习成语怎么办。然后,当然,你开始并努力学习。
请注意,我们在这里只讨论强类型语言:对于强类型语言中的 API 是什么有一个清晰的概念:它是功能(方法)的大杂烩,您可以使用每个功能(方法)和明确定义的协议。
该明确定义的协议通常由方法签名定义。这里 fopen 要求您将一个字符串传递给它(或者在 C 的情况下是一个 char* )。如果你给它别的东西,你会得到一个编译时错误。您没有遵守协议 - 您没有正确使用 API。
在一些(晦涩的)语言中,返回类型也是协议的一部分。如果您尝试在某些语言中调用
fopen()
的等效项而不将其分配给变量,您也会遇到编译时错误(您只能使用 void 函数执行此操作)。我想表达的观点是: 在静态类型语言中,API 程序员鼓励客户正确使用 API,方法是阻止他们的客户代码在出现任何明显错误时进行编译。
(在动态类型语言中,比如 Ruby,你可以传递任何东西,比如一个浮点数,作为文件名——它会编译。如果你甚至不打算控制方法参数,为什么还要用检查异常来烦扰用户。此处提出的论点仅适用于静态类型语言。)
那么,检查异常呢?
那么这里是您可以用来打开文件的 Java API 之一。
看到那个渔获物了吗?这是该 API 方法的签名:
请注意
FileNotFoundException
是一个已 检查 的异常。API 程序员是这样对你说的:“你可以使用这个构造函数来创建一个新的 FileInputStream 但你
a) 必须 以字符串形式传入文件名
b) 必须 接受在运行时可能找不到文件的可能性”
就我而言,这就是重点。
关键基本上是问题所说的“程序员无法控制的事情”。我的第一个想法是他/她指的是 API 程序员无法控制的事情。但事实上,如果使用得当,受检异常实际上应该用于客户端程序员和 API 程序员都无法控制的事情。我认为这是不滥用检查异常的关键。
我认为文件打开很好地说明了这一点。 API 程序员知道您可能会给他们一个文件名,但在调用 API 时发现该文件名不存在,并且他们将无法返回您想要的内容,但必须抛出异常。他们还知道这会经常发生,并且客户端程序员可能希望文件名在他们编写调用时是正确的,但在运行时也可能由于他们无法控制的原因而出错。
所以 API 明确表示:在某些情况下,当您打电话给我时该文件不存在,您最好还是处理它。
这在反例中会更清楚。假设我正在编写一个表 API。我在某个地方有一个带有 API 的表模型,包括这个方法:
现在,作为一名 API 程序员,我知道在某些情况下,某些客户端会为该行传递一个负值或在表外传递一个行值。所以我可能会想抛出一个检查异常并强制客户端处理它:
(当然,我不会真的称它为“已检查”。)
这是对已检查异常的错误使用。客户端代码将充满获取行数据的调用,其中每一个都必须使用 try/catch,这是为了什么?他们是否会向用户报告查找了错误的行?可能不是——因为无论我的表视图周围的 UI 是什么,它都不应该让用户进入请求非法行的状态。所以这是客户端程序员的一个错误。
API 程序员仍然可以预测客户端将编写此类错误,并且应该使用运行时异常来处理它,例如
IllegalArgumentException
。对于
getRowData
中的已检查异常,这显然会导致 Hejlsberg 的懒惰程序员简单地添加空捕获。发生这种情况时,即使对于测试人员或调试客户端开发人员来说,非法行值也不会很明显,而是会导致难以查明来源的连锁错误。 Arianne 火箭将在发射后爆炸。好的,这就是问题所在:我是说检查异常
FileNotFoundException
不仅是一件好事,而且是 API 程序员工具箱中的重要工具,用于以对客户端程序员最有用的方式定义 API .但是CheckedInvalidRowNumberException
是一个很大的不便,导致糟糕的编程,应该避免。但是如何区分呢。我想这不是一门精确的科学,我想这在一定程度上支持并可能证明了 Hejlsberg 的论点。但是我不乐意在这里把婴儿和洗澡水一起扔掉,所以请允许我在这里提取一些规则来区分好的检查异常和坏的检查异常:
仅当错误情况不受 API 和 客户端程序员的控制时,才应使用已检查的异常。这与系统的 开放 或 封闭 程度有关。在客户端程序员可以控制的 受限 UI 中,比方说,在表视图(封闭系统)中添加和删除行的所有按钮、键盘命令等,如果它试图从中获取数据,则它是一个客户端编程错误一个不存在的行。在基于文件的操作系统中,任何数量的用户/应用程序都可以添加和删除文件(开放系统),可以想象客户端请求的文件在他们不知情的情况下被删除,因此他们应该处理它.
不应在客户端频繁进行的 API 调用上使用已检查的异常。经常我的意思是从客户端代码的很多地方 - 不经常及时。因此,客户端代码不会经常尝试打开同一个文件,但我的表视图会通过不同的方法到处都是
RowData
。特别是,我将编写很多代码,例如每次都必须在 try/catch 中包装起来会很痛苦。
在您可以想象向最终用户显示有用的错误消息的情况下,应该使用已检查的异常。这是 “当它发生时你会怎么做?” 我上面提出的问题。它还与第 1 项有关。由于您可以预测客户端 API 系统之外的某些内容可能会导致文件不存在,因此您可以合理地告诉用户:
由于您的非法行号是由内部错误引起的,并且不是用户的过错,因此您实际上没有可以提供给他们的有用信息。如果您的应用程序不让运行时异常进入控制台,它可能最终会给他们一些丑陋的消息,例如:
简而言之,如果您认为您的客户端程序员无法以对用户有帮助的方式解释您的异常,那么您可能不应该使用已检查的异常。
所以这些是我的规则。有点做作,肯定会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,存在像
FileNotFoundException
这样的情况,其中检查的异常与参数类型一样重要和有用,是 API 契约的一部分。所以我们不应该仅仅因为它被滥用而放弃它。抱歉,我不是有意让这件事变得如此冗长和胡扯。最后让我提出两个建议:
A:API 程序员:谨慎使用检查异常以保持其有用性。如有疑问,请使用未经检查的异常。
B:客户端程序员:养成在开发早期创建包装异常(google it)的习惯。 JDK 1.4 及更高版本在
RuntimeException
中为此提供了一个构造函数,但您也可以轻松创建自己的构造函数。这是构造函数:然后养成习惯,每当你必须处理一个已检查的异常并且你感到懒惰(或者你认为 API 程序员一开始就过分热衷于使用已检查的异常)时,不要只是吞下异常,包装它并重新抛出它。
将它放入您的 IDE 的小代码模板之一,并在您感到懒惰时使用它。这样,如果您确实需要处理已检查的异常,您将在运行时看到问题后被迫返回并处理它。因为,相信我(和 Anders Hejlsberg),你永远不会回到你的 TODO