一些关于异常的思考
一些关于异常的思考
异常处理是个非常悠久的话题。最初并不存在异常处理,惯常的方式是像 C 一样,通过不同的返回值标记函数运行的状态。各种语言中常见的返回-1 表示不存在的 indexOf 方法,就是这种方式的遗留。
后来人们发觉,为了代码的健壮性,很多时候需要在 C 语言代码中加入大量的 if…elseif…判断返回状态,以处理各种各样的错误。于是异常处理出现了,它的目的是将处理异常的代码挪到另一处,而在其他地方不用进行各种复杂的返回值判断,只需要抛出异常即可。
异常处理最初并不源于某个特定的编程语言,而是源于早期的一批操作系统。异常处理真正被加入编程语言中的时间,已经很难追溯,但从现在看来,较早加入异常的语言之一是 C++。尽管如此,C++社区当时主要还是维持着从 C 留下来的习惯,C++的异常处理时常为人不屑一顾。异常处理真正被编译器集成,应当源于 Java。
Java 带来了检查型异常,方法将抛出哪些异常都必须在定义时便说明,这使得编译器就可以对异常进行检查,以实现更为健壮的代码。
然而人们逐渐发现,在一些情况下,大量的 try…catch…嵌套并没有减少代码量,反而看起来比 C 语言返回状态的方式还冗长得多。而 Java 的检查型异常更令人难以忍受,尤其是当异常的数量多到一定程度时,许多人宁愿写个空的 catch 块把异常无视也不乐意去进行处理。同时,异常在程序规模增大时似乎并没有很好地使代码更加健壮,因为人们在异常检查上花费了太多的时间,而这些时间本可以节约下来进行更多的单元测试,这能达到同样的目的。
然后我们看到,很多函数式语言并不存在异常,它们使用 Either 来处理类似的问题。Either 是个有些特别的数据结构,它近似于Either[left, right]
,只包含两个值。当函数出错时,就返回Either[error, null]
,左值为异常,右值为空,否则返回Either[null, result]
,左值为空,右值为结果。这样一来,就不需要 try…catch…块的嵌套,一切变得非常清晰。
……好吧,这其实回到了原点。Either 看起来很好,其实和 C 语言惯常的返回状态本质上没什么区别。异常的出现就是为了在编写程序的过程中尽可能不考虑可能出现的错误,然后将处理错误的程序独立到单独的代码块中。要使用 Either,就需要对每个返回值进行判断,这并没有解决问题。
而在一些语言中,干脆既不需要 Either,也不需要异常。例如 Go,函数直接返回两个值,前者是结果,后者是错误,每次使用返回值时都检查一遍是否存在错误(即第二个返回值是否为空)。又如 Node.js,其标准库中最初的一批函数全部返回两个值,区别只是第一个是错误,第二个是结果。可以看到,它们显然也没有解决什么问题,还是像最初 C 语言的思路一样。
Go 的错误处理方式令许多人感到厌恶,因其近乎是强制性地需要程序员每次都考虑错误。而 Python 就显得友好,你可以当作异常不存在——直到你遇到它。
但是异常真的那么重要?在 TDD 模式下,足够的测试似乎已经涵盖了异常,加入大量模板代码处理异常,甚至像 Java 使用编译器检查异常,似乎只是在做重复工作。Python 这样动态的一门语言,仍然能够使用 Django 开发大型网站后端,只要有足够完善的测试。
我们是如此厌恶异常,但又不得不处理异常,正如我们总是一团乱麻的生活一样。