正确处理错误
最后更新于:2022-04-01 05:22:29
使用有两个分支的if语句,只是我的代码可以达到无懈可击的其中一个原因。这样写if语句的思路,其实包含了使代码可靠的一种通用思想:穷举所有的情况,不漏掉任何一个。
程序的绝大部分功能,是进行信息处理。从一堆纷繁复杂,模棱两可的信息中,排除掉绝大部分“干扰信息”,找到自己需要的那一个。正确地对所有的“可能性”进行推理,就是写出无懈可击代码的核心思想。这一节我来讲一讲,如何把这种思想用在错误处理上。
错误处理是一个古老的问题,可是经过了几十年,还是很多人没搞明白。Unix的系统API手册,一般都会告诉你可能出现的返回值和错误信息。比如,Linux的[read](http://man7.org/linux/man-pages/man2/read.2.html)系统调用手册里面有如下内容:
~~~
RETURN VALUE
On success, the number of bytes read is returned...
On error, -1 is returned, and errno is set appropriately.
ERRORS
EAGAIN, EBADF, EFAULT, EINTR, EINVAL, ...
~~~
很多初学者,都会忘记检查`read`的返回值是否为-1,觉得每次调用`read`都得检查返回值真繁琐,不检查貌似也相安无事。这种想法其实是很危险的。如果函数的返回值告诉你,要么返回一个正数,表示读到的数据长度,要么返回-1,那么你就必须要对这个-1作出相应的,有意义的处理。千万不要以为你可以忽视这个特殊的返回值,因为它是一种“可能性”。代码漏掉任何一种可能出现的情况,都可能产生意想不到的灾难性结果。
对于Java来说,这相对方便一些。Java的函数如果出现问题,一般通过异常(exception)来表示。你可以把异常加上函数本来的返回值,看成是一个“union类型”。比如:
~~~
String foo() throws MyException {
...
}
~~~
这里MyException是一个错误返回。你可以认为这个函数返回一个union类型:`{String, MyException}`。任何调用`foo`的代码,必须对MyException作出合理的处理,才有可能确保程序的正确运行。Union类型是一种相当先进的类型,目前只有极少数语言(比如Typed Racket)具有这种类型,我在这里提到它,只是为了方便解释概念。掌握了概念之后,你其实可以在头脑里实现一个union类型系统,这样使用普通的语言也能写出可靠的代码。
由于Java的类型系统强制要求函数在类型里面声明可能出现的异常,而且强制调用者处理可能出现的异常,所以基本上不可能出现由于疏忽而漏掉的情况。但有些Java程序员有一种恶习,使得这种安全机制几乎完全失效。每当编译器报错,说“你没有catch这个foo函数可能出现的异常”时,有些人想都不想,直接把代码改成这样:
~~~
try {
foo();
} catch (Exception e) {}
~~~
或者最多在里面放个log,或者干脆把自己的函数类型上加上`throws Exception`,这样编译器就不再抱怨。这些做法貌似很省事,然而都是错误的,你终究会为此付出代价。
如果你把异常catch了,忽略掉,那么你就不知道foo其实失败了。这就像开车时看到路口写着“前方施工,道路关闭”,还继续往前开。这当然迟早会出问题,因为你根本不知道自己在干什么。
catch异常的时候,你不应该使用Exception这么宽泛的类型。你应该正好catch可能发生的那种异常A。使用宽泛的异常类型有很大的问题,因为它会不经意的catch住另外的异常(比如B)。你的代码逻辑是基于判断A是否出现,可你却catch所有的异常(Exception类),所以当其它的异常B出现的时候,你的代码就会出现莫名其妙的问题,因为你以为A出现了,而其实它没有。这种bug,有时候甚至使用debugger都难以发现。
如果你在自己函数的类型加上`throws Exception`,那么你就不可避免的需要在调用它的地方处理这个异常,如果调用它的函数也写着`throws Exception`,这毛病就传得更远。我的经验是,尽量在异常出现的当时就作出处理。否则如果你把它返回给你的调用者,它也许根本不知道该怎么办了。
另外,try { ... } catch里面,应该包含尽量少的代码。比如,如果`foo`和`bar`都可能产生异常A,你的代码应该尽可能写成:
~~~
try {
foo();
} catch (A e) {...}
try {
bar();
} catch (A e) {...}
~~~
而不是
~~~
try {
foo();
bar();
} catch (A e) {...}
~~~
第一种写法能明确的分辨是哪一个函数出了问题,而第二种写法全都混在一起。明确的分辨是哪一个函数出了问题,有很多的好处。比如,如果你的catch代码里面包含log,它可以提供给你更加精确的错误信息,这样会大大地加速你的调试过程。