编程的智慧 6

正确处理错误

  使用有两个分支的if语句,只是我的代码可以达到无懈可击的其中一个原因。这样写if语句的思路,其实包含了使代码可靠的一种通用思想:穷举所有的情况,不漏掉任何一个。
  
  程序的绝大部分功能,是进行信息处理。从一堆纷繁复杂,模棱两可的信息中,排除掉绝大部分“干扰信息”,找到自己需要的那一个。正确地对所有的“可能性”进行推理,就是写出无懈可击代码的核心思想。这一节我来讲一讲,如何把这种思想用在错误处理上。

  错误处理是一个古老的问题,可是经过了几十年,还是很多人没搞明白。Unix的系统API手册,一般都会告诉你可能出现的返回值和错误信息。比如,Linux的read系统调用手册里面有如下内容:

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,它可以提供给你更加精确的错误信息,这样会大大地加速你的调试过程。

阅读更多

扫码向博主提问

琚建飞

非学,无以致疑;非问,无以广识
去开通我的Chat快问
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页