查看原文
其他

是不是“异常”让我的脑子糊涂了?

Bruce Eckel OSC开源社区 2022-07-03

文末有送书福利哦!

"

《重构:改善既有代码的设计》的作者Martin Fowler曾说过,“异常很不错,但是 Java 的检查型异常带来的麻烦要比好处多。“

这种说法恰好与异常处理的设计理念背道而驰。时至今日,我们需要以一个更谨慎的态度去平衡编程中的得失,需要学会更辩证地思考语言功能设计带给的好处与麻烦,这有助于我们写出更加高效完善的代码。

下文是Bruce Eckel写于2011年的博文,从语言设计层面对比讨论Java与Go异常处理机制的理念,希望能够带给你新的思考。


是不是异常让我的脑子糊涂了?

本文节选自《On Java 中文版 基础卷》

我的朋友James Ward当时正试图编写一些简单易懂的JDBC教学示例,但是检查型异常让他屡屡受挫。他还把Howard Lewis Ship的文章“检查型异常的悲剧”(“The Tragedy of Checked Exceptions”)指给我看。本来应该是很简单的事情,却要克服重重难关,这让他感到沮丧。甚至在finally块中,他也不得不加入更多try-catch子句,因为关闭连接也可能引发异常。这样下去,何时才能到头呢?我们被迫克服重重阻碍,却只是为了做一点简单的事(请注意,try-with-resources语句大大改善了这种情况)。

我们开始讨论Go编程语言,我对这门语言很着迷,因为Rob Pike等人明确提出了关于语言设计的很多基础且深刻的问题。基本上,他们会审视我们已经开始接受的关于语言的一切,然后问一下“为什么”。学习这门语言真的会引发你的思考和好奇心。

我的印象是,Go团队决定不做任何假设,只有在明确“某个功能是必要的”的情况下,才会推动这门语言的演化。他们似乎并不担心所做的修改会破坏旧代码——他们还创建了一个重写工具,所以如果他们做了这样的修改,这个工具可以帮着重写代码。这使他们可以把语言变成一个持续的实验,以发现什么是真正必要的,而不是做大而全的预先设计(Big Upfront Design)。

他们做出的最有意思的决定之一是完全不考虑异常。你没看错,他们不是仅仅舍弃了检查型异常,而是舍弃了所有的异常。

替代方案非常简单,而且乍一看和C语言很像。因为Go从一开始就纳入了元组,所以可以轻松地从一个函数调用返回两个对象:

result, err := functionCall()

(:=操作符让Go在这里定义result和err,并推断出它们的类型。)

每次调用我们都会得到一个结果对象和一个错误对象。我们可以立即检查错误(这非常典型,因为如果某个操作失败了,我们不太可能轻松地进入下一步),或者稍后再检查,如果可行的话。

这乍一看很原始,是向远古时代的倒退。但是现在我发现Go中的这个决策是经过深思熟虑的,并值得玩味。是不是异常让我的脑子糊涂了,我才有这么简单的反应?这对James的问题又有什么影响呢?

我忽然想到,我一直把异常处理看作一种平行的执行路径。如果遇到异常,我们就从正常的路径跳出来,进入这个平行的执行路径,就像一个“奇异世界”(bizarro world),在这里我们不再处理我们所写的东西,而是跳来跳去,进入了catch和finally子句中。正是这一替代性执行路径的世界,引发了James所抱怨的问题。

James创建了一个对象。理想情况下,对象创建不会引发潜在的异常,但是如果会的话,我们就必须捕捉。我们不得不在创建操作之后跟一个try-finally块来确保清理(Python团队意识到,清理并不是一个真正的异常情形,而是一个独立的问题,所以他们创建了一个不同的语言构造——with,以避免二者混淆)。任何会引发异常的调用都会停止正常的执行路径,并(通过平行的奇异世界)跳转到catch子句。

关于异常有个基本的假设:将所有的错误处理代码收集起来放在代码块的末尾,而不是在错误发生的时候直接处理,能够带来某种好处。在这两种情况下,我们都会停止正常的执行,但是异常处理有一个自动机制,可以将我们从正常的执行路径中抛出来,让我们跳入奇异的平行异常世界之中,然后在正确的处理程序中又会把我们送出来。

跳入奇异世界会给James带来问题,而且会给所有的程序员增加更多工作:因为我们无法知道什么时候会发生什么事情(我们随时有可能滑入奇异世界),所以就必须增加一层层的try块,以确保不会有什么东西从裂缝中漏掉。我们最终不得不通过额外的工作来弥补异常机制(这看起来类似于为弥补共享内存并发问题所做的额外工作)。

Go团队做了一个大胆的举动,他们对这一切提出了质疑,并说:“让我们试试没有异常的情况,看看会发生什么。”是的,这意味着我们通常要在错误发生的地方处理它们,而不是把它们全部集中到try块的末尾。但这也意味着关于一件事情的代码都位于一个局部,也许并不是那么糟糕。这可能还意味着,我们不能将公共的错误处理代码合并到一起了(除非识别出公共的代码,并将其放到一个函数中,也不是那么糟糕)。但这肯定意味着,我们不必再担心有多个可能的执行路径,以及由此引发的各种问题了。

你怎么看待编程语言中“异常处理机制”?

福利
包邮送书

为感谢各位粉丝对“OSC开源社区”公众号一直以来的支持,现点击下方公众号聊天窗口回复福利”,将有机会免费领取《On Java》一本~



觉得不错,请点个在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存