泛化對和 ipairs |
|
Lua 5.2 針對在 for 迴圈中控制表的 pairs 和 ipairs 行為,推出了 __pairs 和 __ipairs 方法。__pairs 和 __ipairs 方法和標準反覆器方法類似。以下是無狀態反覆器範例,其行為類似於預設的 pairs 和 ipairs 反覆器,但可以覆寫以取得迴圈行為,例如濾出值或依不同順序迴圈項目。
local M = {} function M.__pairs(tbl) -- Iterator function takes the table and an index and returns the next index and associated value -- or nil to end iteration local function stateless_iter(tbl, k) local v -- Implement your own key,value selection logic in place of next k, v = next(tbl, k) if nil~=v then return k,v end end -- Return an iterator function, the table, starting point return stateless_iter, tbl, nil end function M.__ipairs(tbl) -- Iterator function local function stateless_iter(tbl, i) -- Implement your own index, value selection logic i = i + 1 local v = tbl[i] if nil~=v then return i, v end end -- return iterator function, table, and starting point return stateless_iter, tbl, 0 end t = setmetatable({5, 6, a=1}, M) for k,v in ipairs(t) do print(string.format("%s: %s", k, v)) end -- Prints the following: -- 1: 5 -- 2: 6 for k,v in pairs(t) do print(string.format("%s: %s", k, v)) end -- Prints the following: -- 1: 5 -- 2: 6 -- a: 1
請注意,你可以覆寫字串的 __ipairs,儘管這當然是全域變更
getmetatable('').__ipairs = function(s) local i,n = 0,#s return function() i = i + 1 if i <= n then return i,s:sub(i,i) end end end for i,ch in ipairs "he!" do print(i,ch) end => 1 h 2 e 3 !
Lua 中的 Table 具有下列必要屬性(以及其他屬性)
b=a[x]
和寫入 a[x]=b
。k,v=next(t,k)
(以及較可讀的 for 陳述式與 pairs、ipairs 等)進行 table 反覆。第一個屬性可透過 tables 和 userdata 的 __index 和 __newindex metamethods 來自訂。在 Lua 5.2 之前,沒有直接的方法可以自訂第二和第三個屬性(LuaVirtualization)。我們在此探討自訂這些屬性的方式。
如果你僅對重寫 next()
函數感到滿意,那麼可以使用以下程式碼片段...
rawnext = next function next(t,k) local m = getmetatable(t) local n = m and m.__next or rawnext return n(t,k) end
範例用法
local priv = {a = 1, b = 2, c = 3} local mt = { __next = function(t, k) return next(priv, k) end } local t = setmetatable({}, mt) for k,v in next, t do print(k,v) end -- prints a 1 c 3 b 2
請注意,這不會對 pairs 函數產生影響
for k,v in pairs(t) do print(k,v) end -- prints nothing.
pairs 函數可以根據 next 重新定義
function pairs(t) return next, t, nil end
範例用法
for k,v in pairs(t) do print(k,v) end -- prints a 1 c 3 b 2
ipairs
也可以延伸以參照 __index
metamethod
local function _ipairs(t, var) var = var + 1 local value = t[var] if value == nil then return end return var, value end function ipairs(t) return _ipairs, t, 0 end
範例用法
local priv = {a = 1, b = 2, c = 3, 7, 8, 9} local mt = {__index = priv} local t = setmetatable({}, mt) for k,v in ipairs(t) do print(k,v) end -- prints 1 7 2 8 3 9
以下 C 實作提供類似的行為,但使用 __pairs
和 __index
metamethods。這種使用 __pairs
metamethod 的方式可能比以上使用 __next
metamethod 的另一種方式更快。
此程式碼重新定義 pairs
和 ipairs
,以便
pairs
查閱 __pairs
metamethodipairs
使用 lua_gettable
而非 lua_rawgeti
,以便遵循 __index
metamethods它應該為字串安裝 __pairs
方法,但目前尚未安裝。請隨時新增,所有片段都存在。
它預期會使用 require "xt" 載入,而且會產生包含各種函數的 "xt" table。不過,它也會覆寫全域 table 中的 pairs
和 ipairs
。如果你覺得不妥,請移除 luaopen_xt
中的相關行項目。
簡而言之,取得後按照個人意願處理即可。請隨時貼上補丁程式。
#include "lua.h" #include "lauxlib.h" /* This simple replacement for the standard ipairs is probably * almost as efficient, and will work on anything which implements * integer keys. The prototype is ipairs(obj, [start]); if start * is omitted, it defaults to 1. * * Semantic differences from ipairs: * 1) metamethods are respected, so it will work on pseudo-arrays * 2) You can specify a starting point * 3) ipairs does not throw an error if applied to a non-table; * the error will be thrown by the inext auxiliary function * (if the object has no __index meta). In practice, this * shouldn't make much difference except that the debug library * won't figure out the name of the object. * 4) The auxiliary function does no explicit error checking * (although it calls lua_gettable which can throw an error). * If you call the auxiliary function with a non-numeric key, it * will just start at 1. */ static int luaXT_inext (lua_State *L) { lua_Number n = lua_tonumber(L, 2) + 1; lua_pushnumber(L, n); lua_pushnumber(L, n); lua_gettable(L, 1); return lua_isnil(L, -1) ? 0 : 2; } /* Requires luaXT_inext as upvalue 1 */ static int luaXT_ipairs (lua_State *L) { int n = luaL_optinteger(L, 2, 1) - 1; luaL_checkany(L, 1); lua_pushvalue(L, lua_upvalueindex(1)); lua_pushvalue(L, 1); lua_pushinteger(L, n); return 3; } /* This could have been done with an __index metamethod for * strings, but that's already been used up by the string library. * Anyway, this is probably cleaner. */ static int luaXT_strnext (lua_State *L) { size_t len; const char *s = lua_tolstring(L, 1, &len); int i = lua_tointeger(L, 2) + 1; if (i <= len && i > 0) { lua_pushinteger(L, i); lua_pushlstring(L, s + i - 1, 1); return 2; } return 0; } /* And finally a version of pairs that respects a __pairs metamethod. * It knows about two default iterators: tables and strings. * (This could also have been done with a __pairs metamethod for * strings, but there was no real point.) */ /* requires next and strnext as upvalues 1 and 2 */ static int luaXT_pairs (lua_State *L) { luaL_checkany(L, 1); if (luaL_getmetafield(L, 1, "__pairs")) { lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(L); } else { switch (lua_type(L, 1)) { case LUA_TTABLE: lua_pushvalue(L, lua_upvalueindex(1)); break; case LUA_TSTRING: lua_pushvalue(L, lua_upvalueindex(2)); break; default: luaL_typerror(L, 1, "iterable"); break; } } lua_pushvalue(L, 1); return 2; } static const luaL_reg luaXT_reg[] = { {"inext", luaXT_inext}, {"strnext", luaXT_strnext}, {NULL, NULL} }; int luaopen_xt (lua_State *L) { luaL_openlib(L, "xt", luaXT_reg, 0); lua_getfield(L, -1, "inext"); lua_pushcclosure(L, luaXT_ipairs, 1); lua_pushvalue(L, -1); lua_setglobal(L, "ipairs"); lua_setfield(L, -2, "ipairs"); lua_getglobal(L, "next"); lua_getfield(L, -2, "strnext"); lua_pushcclosure(L, luaXT_pairs, 2); lua_pushvalue(L, -1); lua_setglobal(L, "pairs"); lua_setfield(L, -2, "pairs"); return 1; }
這裡有一個以 Lua 撰寫的替代函式,用來取代 pairs
。注意:與 C 的實作不同,若元表受到保護,這個 Lua 實作將無法使用。
local _p = pairs; function pairs(t, ...) return (getmetatable(t).__pairs or _p)(t, ...) end
-- RiciLake
Beginning Lua Programming 的 262 頁使用類似的方法,但改用 __next
和 __ipairs
元方法。可以在 [1] 的下載連結中找到它的程式碼(請參閱 Chapter 08/orderedtbl.lua)。但請注意,該程式碼需要更正(記載於 306 頁),讓 {__mode = "k"}
成為 RealTbls
、NumToKeys
和 KeyToNums
表的元表。
以下是 RiciLake 撰寫的另一個 OrderedTable 實作。
如果您真的想要仿效這樣的行為,您需要修改 Lua 原始程式碼以加入 lua_rawnext()
,並更新 lua_next()
,若為這種狀況,請參閱 RiciLake 撰寫的 ExtendingForAndNext 條目。
歷史備註:我(RiciLake)在 2001 年 9 月撰寫了 ExtendingForAndNext,當時 Lua 尚未具備通用的 for
陳述式。該設計係根據 Lua 4 中現有的程式碼而來,我希望它對通用 for
陳述式的設計產生了影響,後者在幾個月後便出現了。當時,Lua 使用「標記方法」,而非元方法;標記方法的存取速度會比元方法略快(經過最佳化則例外),但很明顯的是,元方法更好。我僅提出這一點來說明 ExtendingForAndNext 修補程式脈絡中的一些觀念。
ExtendingForAndNext 設想將函式和可迭代物件視為 for
陳述式的目標;然而,Roberto 的設計好上許多,因為它只使用函式。每次迴圈迭代都不會查詢適當的 next
方法,適當的迭代器工廠(例如 pairs
)在迴圈設定時只會呼叫一次。這樣做肯定比較快,除非 next
方法的查詢是微不足道的(通常並非如此)。由於迭代的物件在迴圈中是不變的,next
查詢永遠會是一樣的。但更重要的是,它也比較通用,因為它不會限制可迭代物件只能有一種迭代機制。
我原始提案所處理、而且在目前的 (5.1) Lua 實作中尚未圓滿解決的問題是,雖然對同一個物件有多種迭代機制很方便(string.gmatch
可能就是最好的例子),但要編寫正確的預設迭代機制時,知道一個可迭代物件的類型卻不方便。也就是說,(至少我)想只要寫 for k, v in pairs(t) do
,而不必知道 t
的精確物件類型,而且物件類型有預設的 key/value 類型迭代方法。
上面的程式碼將 pairs
的實作一般化以諮詢 __pairs
元方法,是嘗試以最簡單的方式解決那個問題。
遺憾的是,ipairs
的一般化版本可能不正確,不過因為歷史完整性的關係,我把它留在程式碼中了。通常,你會希望在物件的預設迭代機制應該是遞增整數 key 的情況下,覆寫 ipairs
。實際上,很多情況下給定表格的正確預設反覆運算子是 ipairs
回傳的反覆運算子,而不是 pairs
回傳的反覆運算子,而讓 ipairs
(或客製版本)變成 __pairs
元方法會比較適當,而不是讓物件的用戶端知道預設反覆運算子是 ipairs
。
那個設計幾乎消除了對 __next
元方法的需求,而且我現在個人覺得 __next
是不佳的風格。(實際上,我的感受強烈到要寫這篇很長的註解。)可以主張說,就像有必要能使用 pairs
取得預設反覆運算子,也應該能使用 next
存取預設步驟函式。不過,那會限制 pairs
可行的實作,因為它會強迫它們回傳使用目標物件本體的反覆運算子,而非當成迭代物件的某些委派或代理。在我看來,較佳的風格是使用 pairs
(或其他反覆運算子工廠)回傳三元組 stepfunc、obj、initial_state
,然後用它來手動逐步執行可迭代物件,如果 for
陳述式的嚴謹性不適用時。例如,可以利用這種風格建立一個尾遞迴迴圈
-- This simple example is for parsing escaped strings from an input sequence; the -- iterator might have been returned by string.gmatch function find_end(accum, f, o, s, v) if v == nil then error "Undelimited string" elseif v == accum[1] then return table.concat(accum, "", 2), f(o, s) elseif v == '\\' then s, v = f(o, s) -- skip next char end accum[#accum+1] = v return find_end(accum, f, o, f(o, s)) -- tail recursive loop end function get_string(f, o, s, v) local accum = {v} return find_end(accum, f, o, f(o, s)) end function skip_space(f, o, s, v) repeat s, v = f(o, s) until not v:match"%s" return s, v end function get_word(f, o, s, v) local w = {v} for ss, vv in f, o, s do if vv:match"%w" then w[#w+1] = vv else return table.concat(w), ss, vv end end return table.concat(w) end function nextchar(str, i) i = i + 1 local v = str:sub(i, i) if v ~= "" then return i, v end end function chars(str) return nextchar, str, 0 end function aux_parse(f, o, s, v) while v do local ttype, token if v:match"%s" then s, v = skip_space(f, o, s, v) elseif v:match"%w" then ttype, token, s, v = "id", get_word(f, o, s, v) elseif v:match"['\"]" then ttype, token, s, v = "string", get_string(f, o, s, v) else error("Unexpected character: "..v) end if ttype then print(ttype, token) end end end function parse(str) local f, o, s = chars(str) return aux_parse(f, o, f(o, s)) end parse[[word word2 'str\\ing\'' word3]]
__iter
元方法可以肯定,能夠在表格上如以下方式撰寫一般化 for
會更自然
for item1 in table1 do ... end
一種利用 Lua 5.1 語言定義實現這個目標的慣用語法,是將 __call 元方法用作迭代器工廠函數。然後就能撰寫以下程式碼:
for item1 in table1() do ... end
這樣做比採用 __iter 的方式稍微不自然,但能在不變動語言定義的情況下實作。此外還有一個好處是,可以將參數傳遞到迭代器工廠函數,以修改迭代的行為。例如,ipairs 的版本就有選用的最小和最大索引
for item1 in table1(3, 10) do ... end
如果未來語言實作考量 __pairs 和 __ipairs 元方法,也能夠考量這個 __iter 的替代方案嗎?
(2010 年 1 月 15 日)受夠了對這件事的爭論,決定自己實作!請參閱 LuaPowerPatches。
-- JohnHind
當 t
是表格時,#t
不會呼叫 __len
元方法。請參閱 LuaList:2006-01/msg00158.html。這會妨礙簡單實作一些顯而易見的事情,例如 ReadOnlyTables。
據報導,表格的 __len
元方法已經在 Lua 5.2 中實作(LuaFiveTwo)。
Lua 有一個通則,元方法不會覆寫內建函數,只會在原本會產生錯誤訊息的情況下(或至少會傳回 nil
)提供函數。但是表格的 __len
應該是「證明規則的例外」的合理案例,因為預設行為只適用於連續陣列的特殊情況,可以合理預期在其他情況下會有不同的行為。-- JohnHind
next()