現代程式語言多內建例外處理(Exception handling)機制,目的在讓程式的錯誤發生時,可以有更正式的處理方式。例外處理有如公園中跑步,踏到狗屎會迫使你停下來處理,而不僅是咒罵一聲「Shit Happens!」後,繼續前進。

例外強制程式離開當時執行流程
以C為例,函式執行失敗時的處理方式之一,是傳回錯誤代碼來表示某個錯誤,開發者必須檢查函式傳回值,以判斷錯誤是否發生,然而此方式沒有任何強制性,開發者可能有意或無意忽略了檢查,程式因而持續往下一步運行而進入錯誤流程,就算開發者忠實地檢查錯誤代碼,也會導致商務處理流程中夾雜著錯誤處理邏輯,使得程式碼充滿混亂。

在內建例外處理機制的語言中(以下示範以Java為例),錯誤發生時會丟出(throw)例外,在沒有處理的情況下程式將被迫停止,方法呼叫者必然知道某些錯誤發生。被呼叫的方法中丟出例外,代表著方法撰寫時沒有足夠的前後文(Context)資訊,決定錯誤如何處理,因而將錯誤相關資訊包裝為例外後丟出,由呼叫方法的更高層客戶端來決定。

舉例來說,若要撰寫存取檔案的方法,如果指定的檔案不存在,直接於標準輸出顯示錯誤訊息,也許並不適當,因為該方法可能用於各種環境,因為檔案不存在時如何處理的資訊不足,不應勉強處理錯誤,而應丟出例外,讓方法客戶端去作決策。

如果呼叫某方法時丟出例外,而呼叫者有相關資訊能處理例外,則可將該方法置於try區塊執行,並在catch區塊針對例外加以處理,這使得try區塊中可針對商務邏輯撰寫,而catch中針對錯誤處理撰寫。

以先前檔案存取方法為例,如果呼叫的客戶端是個圖形介面,try中可以撰寫讀取檔案、進行格式處理,而後顯示在編輯區的流程,至於檔案不存在的例外處理,可撰寫在catch區塊中,像是抽取例外中的訊息以顯示錯誤訊息方塊、清除相關資源,或在日誌中加以記錄等。

在例外獲得適當處理的情況下,程式可以回復正常流程。以上例來說,檔案不存在執行完catch區塊後,方法如常返回(return),方法的呼叫端因而可繼續正常流程;另一種情況是,呼叫方法時的前後文資訊僅能處理部份錯誤,此時應在catch中針對擁有的資訊作部份處理,無法處理部份的相關資訊可從例外中抽取出來,建立新的例外重新丟出,或是基於錯誤訊息完整性,將原例外直接丟出,讓之後擁有更多資訊的客戶端,有機會再針對未完部份進行處理。catch中切勿吞掉(swallow)無法處理的部份資訊,甚至完全不處理而吞掉整個例外。

區分受檢與非受檢例外

如果想瞭解方法中可能會丟出哪些例外,最透明的方式就是查看原始碼,瞭解丟出的例外種類,但並不是隨時都有原始碼可以察看。

Java首先採用了受檢例外(Checked),開發者可以使用throws,在方法上聲明丟出的例外種類,方法的客戶端可藉此得知,並針對宣告的例外加以處理,編譯器也會協助,如果方法上宣告的例外是Exception子類別,但非RuntimeException,就會中止編譯來提醒開發者明確處理,如果開發者有相關資訊,就用try……catch處理,無法處理,就繼續在方法上宣告該例外沒有處理。

如果在方法上宣告受檢例外,則暗示著兩件事:方法中忽略了該例外沒有處理、方法的客戶端可能有相關資訊可以處理例外。因而,受檢的例外,應當用來表示程式中可以處理或可以回復程式狀態的錯誤。

相對於受檢例外,Java中RuntimeException則歸屬於非受檢例外(Unchecked),用來表示程式中無法預期的錯誤,或是程式中完全無力處理或回復的錯誤。

簡單來說,發生了非受檢例外,就是程式有臭蟲,基本上不用處理任其往上傳播而中斷程式,最多就是為了除錯方便,在捕捉例外後進行日誌並重新丟出。

舉例來說,如果帳戶實例有個提款方法,若使用者輸入的提款金額超過餘額時,可以提示使用者餘額不足,這個錯誤是可以處理的,因而可以將AccountException設計為受檢例外,在餘額不足時丟出;然而,提款方法傳入的數字應該是正數,如果傳入了負數,表示提款方法的客戶端在呼叫前,並沒有針對提款金額進行檢查,這是一種臭蟲,此時應丟出非受檢的IllegalArgumentException,讓程式停止下來,加入檢查提款金額的相關程式碼,避免呼叫提款方法傳入負數金額的可能性。

在方法上宣告受檢例外時,要注意的是,不同層次的例外應加以區分,例如在DAO(Data Access Object)物件的儲存方法上,宣告SQLException並不適當,這洩漏了底層可能採取的永續(Persistence)機制,如果DAO實作時採用的,並非JDBC,將來可能面臨修改方法宣告,或者是實際上沒有丟出SQLException的問題。

瞭解受檢例外的功與過

如果呼叫的方法宣告了受檢例外,編譯器會提醒呼叫者明確指定處理方式,未指定處理方式的受檢例外,會等同於語法錯誤而造成編譯失敗。說是提醒,其實是強制,有些開發者若想專心撰寫商務流程時,會因為呼叫的方法宣告受檢例外而分心,有些開發者為了先專注於商務流程,隨意地撰寫暫時的try……catch以滿足編譯器,想說之後再回來撰寫catch中真正的錯誤處理,如果最後開發者遺忘了,原先不當的錯誤處理,往往造成更難察覺的臭蟲。

另一個問題是,原先層次較淺的方法,可能因宣告受檢例外而帶來好處,但隨著系統規模與層次的增加,該方法被帶到了較深的層次,或者是應用到另一個既存系統較深層的模組中,編譯器明確提醒受檢例外的好處,就成了麻煩,如果層層呼叫的前後文資訊,都不足以處理受檢例外,那麼要將原受檢例外往上傳播的方式,就是在層層呼叫的每個方法上,都宣告該受檢例外,造成大幅的修改。

如果程式規模擴大,異常處理的方式也應跟著演化,原先的受檢例外也許應考量是否演化為非受檢例外。如果層層呼叫的前後文資訊,都不足以處理受檢例外,代表著對於呼叫的每一層來說,該例外都是代表無力處理的錯誤。

將受檢例外改為非受檢例外的方式之一,就是改繼承RuntimeException,Java永續框架Hibernate就是這麼作的。在3.0版本之後,將其HibernateException從受檢例外改為非受檢例外,畢竟對於資料永續的相關方法而言,多數錯誤都是無力處理的,最好的方式就是任其向上傳播。

另一種方式是抽取出原受檢例外的資訊,重新包裝至其他非受檢例外再丟出,使之直接往上傳播。

即便如此,有人認為受檢例外帶來的麻煩遠比好處還多,像是物件導向大師Martin Fowler就如此認為,有些程式庫放棄使用受檢例外,與Java血緣相近的Scala語言亦是如此,將例外的處理權交回給開發者,而不是由編譯器強制規範。

瞭解錯誤種類與處理錯誤的前後文資訊
有人研究過,程式中可能會有高達90%的比率,在管理與處理錯誤;軟體開發中或許只有相反比率的書籍或文件,討論過如何處理錯誤。Java對受檢例外與非受檢例外的區分,其實是在迫使開發者思考開發程式時可能面對的錯誤,哪些是可處理的一般問題,哪些則是不可處理的異常狀況。

撇除受檢例外與非受檢例外的差異不談,處理錯誤時,本應思考錯誤發生時的前後文資訊如何處理,有多少前後文資訊就處理多少錯誤,無法處理的部份就應丟出,而面對不可處理的例外,應思索該錯誤是否為可用程式邏輯避免的臭蟲,而不是在catch之後勉強進行回復,這容易使得catch被誤用來進行商務邏輯,失去catch用於錯誤處理的本意。

 

作者簡介


Advertisement

更多 iThome相關內容