儘管毀譽參半,許多語言都有例外處理相關語法,因而現代開發者多半熟悉例外處理,若有個新興語言沒有內建例外語法,反而會引起一番討論。

反過來想,在內建例外處理的語言特意不使用例外語法,又想要有例外處理的特性,我們該如何實現呢?

而在實作語言的層面上,關於例外處理又是怎麼一回事?

如果沒有例外處理

在沒有例外處理語法的語言中,若開發者想要讓函式呼叫端知道,這個函式執行時發生了一些問題,方式就是傳回錯誤訊息。

若是動態定型語言,由於傳回值可以是任意形態,這個動作比較不成問題;若是靜態定型語言,由於多數語言不允許傳回多個值,若要能同時有正確傳回值與錯誤訊息,必須自行封裝為物件(或結構)後傳回,函式呼叫端必須檢查傳回物件中是否有錯誤訊息,從而決定是否要進行錯誤處理。

在純函數式的世界中沒有例外處理,就經常運用此模式,現代開發者熟悉的Option也是此模式的實現之一,若要令語義更為清晰,可以進一步使用Either,或甚至實現Scala中的Try等API,先前專欄〈函數式風格錯誤處理〉中,我就曾經探討這些API的來由與應用。

相對年輕的Go語言,並沒有例外處理,然而,Go允許函式同時傳回多個值,內建函式執行若可能發生錯誤,傳回值會有兩個,成功的傳回值與失敗時的錯誤值,關於這部份,我先前也在〈Go的error與panic〉做過討論。

然而,例外處理的方便性,並不在於錯誤值的檢查,而是在於例外丟出時,「自動」收集環境資訊,並中斷後續各層的函式呼叫,如果想以Go語言來實現這個功能,在不使用panic的情況下,就必須在讓每個函式都傳回兩個值,並在每個呼叫點,都加上錯誤檢查:

result, error := f()
if error != nil {
// 將環境相關資訊收集至error
return nil, error
}

若想要處理錯誤,可以在if檢查區塊中進行,這就相當於捕捉(catch)例外了,若想要再度拋出例外,可以於上頭的if中,再撰寫以下內容:

err = catch(error)
if(err) {
return nil, err
}

if/else與return

如果沒有例外語法,想要能有例外傳播的效果,就必須「手動」進行,方式就是自行使用if檢查錯誤,並逐一return。這當然是很麻煩的過程,特別是在專案程式碼已經有相當規模的情況下,若有個底層錯誤想要往上傳播,逐層加上這類的程式碼,就會成為艱難的任務,因此,會希望能有某種機制,可以自動化這個過程。

既然我們可以手動地加入if、return,來達到例外處理的效果,此時,開發者也許會聯想到,try/catch語法是否會是一種if/else,而throw則是一種return呢?

在探討這個問題之前,我們必須先知道if/else、return在語言實作上,是如何實現,由於實作語言時,陳述句(Statement)是運行的單元之一,return在簡單的實現上會是:

const ctx = this.current.evaluate(context);
if(ctx.returnValue === null) {
return this.rest.evaluate(ctx);
}
return ctx;

如果current是個return陳述句,傳回的環境物件ctx,就會帶有returnValue,此時,就不會執行後續的陳述句,而這就是return會中止函式執行流程的原理,就效率上,可以不用每次都檢查returnValue,不過,上面的方式比較易於理解。

雖然程式碼上,if/else會是兩個區塊,然而在if/else的實現上,只要一個If節點,就可以了。

這個節點會包含條件成立,以及不成立時要執行的陳述句,開發者若曾實現過DSL,應該不難想像,就像有個ifFunc(predict, trueFunc, falseFunc),predict為true、會呼叫trueFunc,否則呼叫falseFunc。

try/catch與throw

現在,可以為語言加上throw關鍵字了,當執行到throw陳述時,傳回的環境物件ctx,可以帶有throwValue,這時,就不會執行後續的陳述句,因此實現的原理與return相同:

const ctx = this.current.evaluate(context);
if(ctx.throwValue === null) {
return this.rest.evaluate(ctx);
}
return ctx;

在執行語法樹中的函式呼叫節點時,則必須檢查傳回的環境物件是否有throwValue,若有就直接傳回環境物件,如此一來,每個帶有函式呼叫節點的陳述,檢查到的throwValue都不會是null,如此不斷重複,就可以達到往上傳播例外的效果。

如果想要中止傳播例外,必須使用try陳述句來包含throw陳述句,如果有try中發生例外,Try語法樹節點中在執行流程上類似If節點:

const ctx = this.tryStmt.evaluate(context);
if(ctx.throwValue !== null) {
return funCall(this.catchStmt, ctx.throwValue).evaluate(ctx);
}
return ctx;

catch會有名稱參考至例外物件,因此,可以隱含地建立一個函式,將例外物件設為函式的引數,這麼一來,也就允許catch區塊中可擁有return陳述,由於函式會有自己的環境物件,在catch處理過後,該環境物件就不會有throwValue,此時,也就可以中止例外的傳播。

語法還是想法?

如果將語言實作看成是框架,程式碼就像是用來呼叫框架中各元件的字串引數,在瞭解到例外是如何在語言中實作出來之後,若語言本身內建例外,該自行檢測傳回值是否有錯誤,或者是使用例外自動傳播,就會是一種想法上的選擇了。

當然,語言若內建相關語法,無論選擇哪個想法都會易於實現,這絕對是件好事,然而不該僅受限於語法,真正重要的,還是有無認真思考過,哪些是可處理的例外,而哪些不是,可處理的例外又該如何處理,是否多方思考那些談論例外處理的文件提及的議題。

如此一來,就能更進一步瞭解到如何善用既有的錯誤處理語法,就算語法上沒支援,也可以依特定需求,創造出自己的錯誤處理機制;相反地,若不知思考,如先前所提,例外處理是種「自動」傳播,如果不想要這種自動化,卻用了例外語法(或意外引發例外),只是徒生困擾罷了!

作者簡介


Advertisement

更多 iThome相關內容