已完成的例外 |
|
assert
和 pcall
函式之間。為了讓它更明顯,我們使用 Lua 函式回傳值的一個方便標準(你可能已經在使用了),並定義了兩個非常簡單的輔助器函式(在 C 或 Lua 本身中定義)。
大多數 Lua 函式在發生錯誤時會傳回 nil
,接著是一個訊息用於描述錯誤。如果你沒有使用此慣例,那你可能自有道理。希望在讀完之後,你會了解到你的理由不夠充分。
如果你和我一樣,你討厭錯誤檢查。大多數令人滿意的簡短程式碼片段在你第一次寫程式碼時看起來很漂亮,但是當你加入所有那些錯誤檢查程式碼時,它們就會失去一些魅力。然而,錯誤檢查和程式碼的其他部分一樣重要。多麼悲傷啊。
即使你堅持回傳慣例,任何包含數個函式呼叫的複雜任務都會讓錯誤檢查既繁瑣又容易出錯(你看到下面的 錯誤 嗎?)
function task(arg1, arg2, ...) local ret1, err = task1(arg1) if not ret1 then cleanup1() return nil, err end local ret2, err = task2(arg2) if not ret then cleanup2() return nil, err end ... end
標準 assert
函式提供了一個有趣的替代方案。若要使用它,只需將每個要進行錯誤檢查的函式呼叫嵌套到對 assert
的呼叫即可。assert
函式檢查其第一個引數的值。如果它是 nil
,assert
會將第二個引數當作錯誤訊息引發。否則,assert
會讓所有引數通過,就像沒有它一樣。這個概念大大簡化了錯誤檢查
function task(arg1, arg2, ...) local ret1 = assert(task1(arg1)) local ret2 = assert(task2(arg2)) ... end
如果任何任務失敗,assert
會中止執行,並將錯誤訊息顯示給使用者作為問題的原因。如果沒有發生錯誤,任務就會像以前一樣完成。沒有一條 if
語句,這太棒了。但是,這個概念有一些問題。
首先,最上層的 task
函式不遵循下層級函式遵循的協定:它會引發錯誤,而不是傳回 nil
接著是錯誤訊息。標準 pcall
就是在這裡派上用場。
function xtask(arg1, arg2, ...) local ret1 = assert(task1(arg1)) local ret2 = assert(task2(arg2)) ... end function task(arg1, arg2, ...) local ok, ret_or_err = pcall(xtask, arg1, arg2, ...) if ok then return ret_or_err else return nil, ret_or_err end end
我們新的 task
函式表現得很好。Pcall
會擷取由對 assert
的呼叫引發的任何錯誤,並在狀態碼後傳回它。這樣一來,錯誤就不會傳播到 task
高階函式的使用者。
以下是我們例外方案的主要概念,但仍有幾個小問題需要修復
pcall
破壞了程式碼的簡潔性;Assert
在引發錯誤之前會混淆錯誤訊息(它會新增列號碼資訊)。幸運的是,所有這些問題都很容易解決,而那就是我們在以下各節所做的。
protect
工廠我們使用pcall
函式來保護使用者免於底層實作所引發的錯誤。每次我們都偏好使用一個執行相同工作的工廠,而不是直接使用pcall
(因此重複程式碼)
local function pack(ok, ...) return ok, arg end function protect(f) return function(...) local ok, ret = pack(pcall(f, unpack(arg))) if ok then return unpack(ret) else return nil, ret[1] end end end
protect
工廠接收一個可能引發例外狀況的函式,並傳回一個遵守我們的回傳值慣例的函式。現在我們可以用更乾淨的方式重寫最上層的task
函式
task = protect(function(arg1, arg2, ...) local ret1 = assert(task1(arg1)) local ret2 = assert(task2(arg2)) ... end)
protect
工廠的 Lua 實作會在建立表格來容納多個參數和回傳值時受限。在 C 中實作相同的函式(不建立任何表格)是可能的(而且容易)。
static int safecall(lua_State *L) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); if (lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0) != 0) { lua_pushnil(L); lua_insert(L, 1); return 2; } else return lua_gettop(L); } static int protect(lua_State *L) { lua_pushcclosure(L, safecall, 1); return 1; }
在 Lua 5.1 開始,暫時表格也可以在純 Lua 程式碼中避免。
do local function fix_return_values(ok, ...) if ok then return ... else return nil, (...) end end function protect(f) return function(...) return fix_return_values(pcall(f, ...)) end end end
newtry
工廠讓我們使用單一策略解決剩餘的兩個問題,並使用具體範例來說明建議的解決方案。假設你想要寫一個用來下載 HTTP 文件的函式。你必須連接、傳送要求並讀取回覆。每個任務都可能失敗,但如果在連接之後某件事出錯,你必須在傳回錯誤訊息之前關閉連接。
get = protect(function(host, path) local c -- create a try function with a finalizer to close the socket local try = newtry(function() if c then c:close() end end) -- connect and send request c = try(connect(host, 80)) try(c:send("GET " .. path .. " HTTP/1.0\r\n\r\n")) -- get headers local h = {} while 1 do l = try(c:receive()) if l == "" then break end table.insert(h, l) end -- get body local b = try(c:receive("*a")) c:close() return b, h end)
newtry
工廠會傳回一個像assert
一樣運作的函式。不同之處在於try
函式不會混淆錯誤訊息,而且它會在引發錯誤之前呼叫選擇性的完成函式。在我們範例中,完成函式會簡單關閉 socket。
即使使用像這樣的簡單範例,我們也可以看出最後化的例外簡化了我們的生活。讓我們來看看我們在一般情況下可以獲得什麼,而不仅仅是這個範例
試著不要使用我們在上面使用的技巧來寫相同的函式,你會發現那個程式碼很醜陋。使用錯誤檢查的更長的運算順序會變得更醜陋。因此我們在 Lua 中實作newtry
函式
function newtry(f) return function(...) if not arg[1] then if f then f() end error(arg[2], 0) else return unpack(arg) end end end
而且,這個實作會在每個函式呼叫時受限於表格建立,因此我們偏好 C 版本
static int finalize(lua_State *L) { if (!lua_toboolean(L, 1)) { lua_pushvalue(L, lua_upvalueindex(1)); lua_pcall(L, 0, 0, 0); lua_settop(L, 2); lua_error(L); return 0; } else return lua_gettop(L); } static int do_nothing(lua_State *L) { (void) L; return 0; } static int newtry(lua_State *L) { lua_settop(L, 1); if (lua_isnil(L, 1)) lua_pushcfunction(L, do_nothing); lua_pushcclosure(L, finalize, 1); return 1; }
而且,從 Lua 5.1 開始,在純粹 Lua 程式碼中可以用有效率而且沒有暫時表格的方式進行實作
function newtry(f) return function(ok, ...) if ok then return ok, ... else if f then f() end error((...), 0) end end end
protect
和 newtry
函式幫 LuaSocket
[1] 的實作省下了大量的工作。透過這些構想,有些模組的程式碼量減半。雖然這個策略沒有 C++ 或 Java 等程式語言的例外機制通用,但它的功能與易用性比值相當理想,我希望它對你們的輔助效果就和 LuaSocket
一樣好。