程式語言中的錯誤處理
在日常的程式設計過程中,不可避免地需要處理錯誤的情況,而每一種程式語言都自有其錯誤處理邏輯,其背後的考量是什麼?下面來探討一下各程式語言中的錯誤處理,嘗試總結出一些通用的方法與原則。
一、什麼是異常
討論一個問題之前,第一步就是要明晰下它所涉及的概念。
首先,標題所說的錯誤是廣義的錯誤,它包括異常(Exception)與錯誤(Error)。下文中提到的『錯誤』均為狹義的區別與異常的錯誤。
程式中的異常(Exception)是指發生在程式執行過程中非頻繁非正常的事件,它位於程式正常流程之外。
異常大致可分為兩類:
- 硬體異常:由CPU發起,它們可能是某些指令序列的執行導致的。比如除零或訪問非法記憶體地址等。
- 軟體異常:由應用程式或作業系統顯式發起。例如系統可以檢測到指定的引數非法值。
程式語言中的異常則屬於軟體異常。
而程式中的錯誤(Error),通常是指發生在程式執行過程中正常的事件,它就在程式正常流程範圍之內。
二、正確區分異常與錯誤
與異常概念最容易混淆的就是錯誤。
二者通常可以通過下面三個維度來區分:是否正常/可預期/終止程式
概念 | 是否正常 | 是否可預期 | 是否終止程式 |
---|---|---|---|
異常 | 否 | 否 | 是 |
錯誤 | 是 | 是 | 否 |
前兩個維度主要是對概念的描述,最後一個維度(是否終止程式,即是否可恢復)建議作為定義錯誤與異常的標準。如果一個事件它不可恢復應該定義為異常,及時終止程式退出,避免程式進入不可預知的狀態(如造成資料不一致);如果一個事件可以預測出錯誤,那麼就應該check,並做一些相應的恢復處理。如Golang中的Error與Panic就是遵循該原則而設計。
三、錯誤處理
區分了異常與錯誤,下一步則是考慮針對錯誤的處理機制。
正確地區分了異常與錯誤的概念,我們就可以根據具體場景,正確地定義出異常與錯誤,以及安排相應的錯誤處理。
通常,程式語言中的錯誤處理可以分為兩類:
- check式,檢查返回值,以C語言為代表,Go亦如此;
- try/catch式,目前大部分主流程式語言中的異常處理均採用類似方式,如C++/Java/PHP/JavaScript等。
雖然目前主流程式語言中的異常處理均採用『try/catch』式的原則,但是大都數在寫程式碼過程中都是雙管齊下的,依據具體的場景,選擇合適的處理方式(拋異常 or 檢查返回值)。
3.1 check式
最早的C語言是通過檢查函式返回值(通常零值/非空成功;非零值/空失敗)來進行錯誤處理的。
如定義一個函式:
int foo() { // <try something here> if (failed) { return 1; } return 0; }
呼叫者則在進行下一步操作之前,需要判斷foo函式返回值:
int err = foo(); if (err) { // Error!Deal with it. }
基於C或者底層級別的系統均是通過這種檢查返回值的方式來處理錯誤的。如Window和Linux作業系統級別的呼叫(API)。
這種方式很簡單,程式碼可讀性也較好,但是寫起來非常繁瑣,這意味著你需要對每一個函式在呼叫之前的都需要手動check一下。而且,一旦忘記檢查,很容易出現bug。
Golang則在C語言的基礎上增加了更符合現代程式語言的語法和庫。它允許函式有兩個返回值,通常最後一個返回值為Error型別,呼叫者可以通過檢查該型別返回值來檢查函式返回情況,沒有錯誤則使用第二個返回值,繼續接下來的業務邏輯操作。
如:
func foo() (int, error){ // <try something here> if (failed) { return -1, errors.New("something error") } return 0, nil; }
呼叫:
if sum, err := foo(); err != nil { // Deal with the error. } // do something with sum ...
Golang的實現方式看起來比C語言更加優雅一些,但是頻繁地檢查返回值仍然不可避免。
C語言在不使用goto語句的情況下,異常程式碼複用幾乎不可能,Golang也難以解決這個問題。
於是在後來發展起來的面向物件程式語言中,大部分都引入了類似try/catch式的異常處理機制。
3.2 try/catch式
下面主要以Java語言舉例說明
Java中所有的錯誤處理均基於Throwable頂層父類,其下有兩個子類:Error,它表示不希望被程式捕獲或者是程式無法處理的錯誤。另一個是Exception,它表示使用者程式可能捕捉的異常情況或者說是程式可以處理的異常。
這是Java對異常與錯誤的劃分,並且在此基礎上為進一步提高程式健壯性,引入了checked異常與unchecked異常概念:針對那些除了RuntimeException與其子類,以及錯誤(Error),其他的都是需要編譯時強制檢查的異常,否則編譯器會報錯。
try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }
也正是Java的這種處理方式讓人詬病:checked異常容易讓相關程式碼裡充斥著大量的try/catch,使程式碼同樣變得晦澀難懂。同時,checked異常所起到的作用也只是將捕獲的異常,包裝成執行時異常,然後再重新丟擲。
正如,前文所言,每一門程式語言在設計之初都有自身的考量,且在進行實際的錯誤處理時均會同時考慮『check』與『try/catch』兩種方式。
在C#中,並沒有引入checked異常概念,而是把檢查的義務又『還給』了開發者。
除此之外,try/catch式異常處理通常會有很大的效能開銷,故應當慎用。
四、『check』 OR『try/catch』
即使發展至今,關於異常處理(『try/catch』)與檢查返回值(『check』)這兩種錯誤處理方式仍然爭議不斷。
check式與try/catch式兩種錯誤處理的方式,沒有哪一種是絕對優勢的,都有各自的優缺點,這有賴於語言設計者當時的權衡與抉擇。但是不管哪種程式語言,基本衍生於這兩類處理方式。