使用有兩個分支的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,它可以提供給你更加精確的錯誤信息,這樣會大大地加速你的調試過程。