遞延呼叫 |
|
有時需要在程式中一個地方定義函式呼叫,但也需要將其執行遞延到稍後時間。例如,事件驅動系統需要序列化對事件非同步發生的回應。我們需要在事件發生時對應答函式進行排隊,然後在對較早(或優先順序較高)的事件的回應完成後對其進行出隊和執行。如果回應函式沒有參數,這很直接 - 只需將函式儲存在表格中即可。然而,如果回應函式需要參數進一步描述事件,則會出現問題 - 參數在函式排隊時已知,但在函式執行時卻未知。
這是使用封閉的典型範例,如果整個系統是用 Lua 編寫的,這就很簡單。但是,我們可能希望使用 C 編寫排隊系統,而僅使用 Lua 編碼事件回應函式。由於透過 C API 無法直接使用封閉式函式,就像在 Lua 中使用詞彙範圍一樣。
這裡介紹的方案使用 C 封閉式呼叫來封裝任意 Lua 函式及其呼叫參數。當呼叫 C 封閉時,它會從其上值中復原 Lua 函式及其參數,進行呼叫並傳回結果。由於 C 封閉僅是 Lua 函式,因此可以儲存在註冊表或任何其他表格中。
這個方案也可以在 Lua 中浮現出來,以簡化使用閉包來建立遞延函式呼叫,如果我們首先從 Lua 來處理這個問題,會最容易理解。
稍後呈現的 C 程式碼會發布新的全域函式 createdeferredcall,其用法如下
local func = function(p1, p2, p3) print("func", p1, p2, p3); return "ret1","ret2"; end local dfunc = createdeferredcall(func, "call1", "call2", "call3")
dfunc 只是一個保存 Lua 函式的變數,但這個函式現在封裝了原始函式 func 及其三個字串參數的值(可以有任何數量的任何 Lua 類型的參數)。我們可以將它視為與任何 Lua 變數完全相同。稍後,當要執行函式時,我們只需呼叫 dfunc 而無需參數
print(dfunc())
會出現以下結果
>func call1 call2 call3 >ret1 ret2
如果呼叫期間發生錯誤,系統會報告錯誤並終止,與正常的呼叫操作類似。但是,我們可以將錯誤函式傳遞到呼叫,然後它將表現得像 pcall
print(dfunc(function() print("inerrorfunc"); return "errormess"; end))
這會出現以下結果
>func call1 call2 call3 >true ret1 ret2
如果在 func 中引發錯誤,呼叫會傳回布林值 false,後跟錯誤函式提供的訊息
>inerrorfunc >false errormess
這在概念上和實務上比在標準 Lua 中使用字彙範圍產生閉包更直接。不過,真正的優點在於 *createdeferredcall* 也可用作新的 C API 函式,讓我們可以執行過去無法執行的操作,也就是透過 Lua 中定義的函式在 C 中建立閉包。
以下是 C 程式碼
static int Lua_DcallDelegate(lua_State* L) { int efun = (lua_isfunction(L, 1))? 1 : luaL_optint(L, 1, 0); int nret = luaL_optint(L, 2, LUA_MULTRET); lua_checkstack(L, 1); lua_pushboolean(L, TRUE); int sm = lua_gettop(L); int ix = 1; while (!lua_isnone(L, lua_upvalueindex(ix))) { lua_checkstack(L, 1); lua_pushvalue(L, lua_upvalueindex(ix++)); } ix--; if ((ix < 1) || (!lua_isfunction(L, (-1 * ix)))) return luaL_error(L, "Bad Deferred Call"); if (lua_pcall(L, ix - 1, nret, efun) == 0) return (efun == 0)? lua_gettop(L) - sm : lua_gettop(L) - sm + 1; else { lua_pushboolean(L, FALSE); lua_replace(L, sm); return (efun == 0)? lua_error(L) : 2; } } static void luaX_pushdcall(lua_State* L, int nargs) { luaL_checktype(L, 1, LUA_TFUNCTION); lua_pushcclosure(L, Lua_DcallDelegate, nargs + 1); } static int luaX_dcall(lua_State* L, int nresults, int errfunc) { lua_checkstack(L, 2); lua_pushinteger(L, errfunc); lua_pushinteger(L, nresults); return lua_pcall(L, 2, nresults, errfunc); } static int LuaMakeDeferredCall(lua_State* L) { luaX_pushdcall(L, lua_gettop(L) - 1); return 1; } lua_pushcfunction(L, LuaMakeDeferredCall); lua_setglobal(L, "makedeferredcall");
*luaX_pushdcall* 和 *luaX_dcall* 是兩個新的 API 函式,它們密切地遵循 *lua_call* 使用的慣例。*luaX_pushdcall* 需要函式堆疊在 *nargs* 參數的下方,且最後一個參數在堆疊最上方。結束時,這些參數會彈出,而函式則會更換成擁有 *Lua_DcallDelegate* 函式的 C 閉包。*luaX_dcall* 需要 C 閉包放在堆疊的最上方。至於 *lua_call*,*nresults* 會指定需要取用的回傳參數數量(它可以是 LUA_MULTRET 鍵)。參數 *errfunc* 可設定為 0 來指定沒有錯誤函式或指定錯誤函式在堆疊中的位置,這和 *lua_pcall* 類似。
如果 *errfunc* 為 0,封裝函式中的任何錯誤都是終止性的,否則回傳參數會在堆疊中,其最後一個參數在堆疊的最上方。
如果已識別出 *errfunc*,封裝函式中的任何錯誤都會導致 *luaX_dcall* 在 *errfunc* 所回傳的字串訊息下回傳布林值 *false*,否則,它會在回傳參數下回傳布林值 *true*,而最後一個參數在堆疊的最上方。
使用 LuaK,它是支援延期語意的完整 Lua 5.x 版本。
例如
function foo() defer defer print("defer in defer") end print("defer") end print("foo") end foo() => output foo defer defer in defer
原始碼:git@github.com:peterk9999/LuaK.git
如果你想查看這個版本和 lua 5.1.1 之間的差異,請參閱此 git@github.com:rst256/luaK__diff.git 這不是分岔,也不是分支,這個儲存庫只用於查看差異。實際上,程式碼是在 15.03.2015 13:17:43。