編程中錯誤處理的方法
編程語言提供的意外情況處理的機制大概分為兩種:1. 函數返回「錯誤感知(error-aware)類型」的值;2. 拋出異常。
函數返回「錯誤感知類型」的值
在函數返回值中包含錯誤,這要求函數的調用者顯式地處理這個錯誤,調用者可以選擇從錯誤中恢復,或者繼續傳播這個錯誤。使用這種方法的一個重要前提是,函數的值必須被處理。在函數式風格的代碼中,(幾乎)所有的返回值都會被處理,但是在命令式風格的代碼中,函數的返回值很容易被忽視掉。
具體到如何使用函數的返回值來表示意外情況,目前存在的一些方法有:
- 使用一些「特殊」的值。例如,很多C函數通過返回-1或null來表示意外情況。這樣一來,用於表示預期結果的值和用於表示意外情況的值就很容易混淆。
- 通過多返回值的方式把預期結果和用於表示意外情況的值分離開來,這就是Golang的做法。在Golang里的寫法大概是
result, err := f()
,result和err是兩個變數,對result的使用不依賴對err的處理,所以這種做法其實並不嚴謹。 - 通過類型把預期結果和可能發生的意外情況組合在一起,想要取出預期結果必須先對可能發生的意外情況進行處理。例如OCaml的option類型、Typed Racket的Union type。
前兩種方法都是不嚴謹的,所有後文只談第三種。
拋出異常
函數返回「錯誤感知類型」的值帶來了一些麻煩,每一次函數調用都必須對返回值進行檢查。所以很多現代化的語言都提供了異常機制,允許你「拋出異常」,函數的調用者可以「捕獲」這個異常,對其進行處理,也可以忽視它,任其繼續傳播,直至「頂層」。
之前寫了一篇文章,通過自己動手實現異常機制來理解異常。看完那篇文章之後應該能理解異常和普通的函數返回值之間的區別和聯繫。
孤獨的Ziv:通過自己實現來理解異常在兩種方式之間切換
大部分提供了異常機制的語言都允許你在兩種處理意外情況的方式之間切換,在兩者之間做選擇的時候需要考慮在簡潔性和明確性之間權衡。
異常比較簡潔,因為異常允許把錯誤處理的工作延遲到一個合適的地方,而不需要調用鏈上的每一個函數都處理一遍。而且,異常使用的是「nonlocal return」,不會和「local return」的類型混雜在一起。
和異常不同的是,錯誤感知類型則在類型定義中有充分的描述(不適用於C語言),使得代碼可能產生的錯誤很明確,不會被忽略。
如何做出合適的權衡呢?這取決於你的具體應用。
下面的情況可能適合大量使用異常:
- 任務是輕量級的,而且有「監督者」負責監控任務的狀態並在任務失敗後自動重啟;
- 「微服務」;
- 程序宕掉的成本不高;
- 「軟實時」系統;
下面的情況可能需要使用錯誤感知返回類型:
- 傳統的「大型軟體系統」的架構(有疑問?請繼續看下一節);
- 程序宕掉的成本很高;
- 「硬實時」系統;
融合兩種方式
使用錯誤感知返回類型要求函數的調用者必須處理意外情況,而且在類型定義中充分描述了可能產生的錯誤。異常機制帶來了更多的靈活性,但是也帶來了一些弱點:類型系統不知道函數可能拋出什麼異常,異常容易被忽略掉。
Java這門語言試圖對異常的弱點進行修復,它融合了上面的兩種方式,在Java的設計中有一些特色:
- 必須給異常寫類型標記(throws),而且函數/方法的普通返回值類型和「nonlocal return」(異常)的類型標記是分開的;
- 不允許「unhandled exception」,所有能處理的異常都必須被處理。
這樣一來,Java既避免使用錯誤感知返回類型時類型混雜的問題,也避免了catch時無法得知到底要catch哪些異常的問題,還避免了異常被忽略的問題。
但是,Java也繼承了使用錯誤感知返回類型的矯枉過正的問題,而且,在Java中無法在兩種方式之間自由選擇切換。
推薦閱讀: