泛型对和 Ipairs |
|
Lua 5.2 引入了 __pairs
和 __ipairs
方法,用于控制 for
循环中表对 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 中的表具有以下基本属性(以及其他属性)
b=a[x]
和写入 a[x]=b
。
k,v=next(t,k)
进行表迭代(以及更易读的带有 pairs
、ipairs
和其他内容的 for
语句)。
第一个属性可以通过表和用户数据的 __index
和 __newindex
元方法进行自定义。在 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
元方法。
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
元方法。这种使用 __pairs
元方法的方法可能比上面使用 __next
元方法的替代方法更快。
此代码重新定义了 pairs
和 ipairs
,以便
pairs
查询 __pairs
元方法
ipairs
使用 lua_gettable
而不是 lua_rawgeti
,以便它将尊重 __index
元方法
它应该为字符串安装一个 __pairs
方法,但它还没有。请随时添加它,所有部分都在那里。
它期望使用 require "xt"
加载,并将生成一个带有各种函数的 "xt" 表。但是,它还会覆盖全局表中的 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; }
以下是 pairs
替换的另一个 Lua 实现。注意:与 C 实现不同,此 Lua 实现如果元表受到保护将不起作用。
local _p = pairs; function pairs(t, ...) return (getmetatable(t).__pairs or _p)(t, ...) end
-- RiciLake
Lua 编程入门 第 262 页使用类似的方法,但使用 __next
和 __ipairs
元方法。代码可以通过 [1] 中的下载链接在线获取(参见第 08 章/orderedtbl.lua)。但是,请注意,此代码需要进行修正(在第 306 页中指出),以使 {__mode = "k"}
成为 RealTbls
、NumToKeys
和 KeyToNums
表的元表。
如果您想真正模拟这种行为,您需要修改 Lua 源代码以添加 lua_rawnext()
并更新 lua_next()
,在这种情况下,请参阅 RiciLake 的 扩展 for 和 next 条目。
历史说明:我(RiciLake)在 2001 年 9 月编写了 扩展 for 和 next,当时 Lua 还没有通用的 for
语句。该设计基于 Lua 4 中的现有代码,我认为它影响了通用 for
语句的设计,该语句在几个月后出现。当时,Lua 有“标签方法”而不是元方法;标签方法访问比元方法访问快一些(除了优化的地方),但元方法显然更好。我提到这一点只是为了将 扩展 for 和 next 补丁置于上下文中。
扩展 for 和 next 考虑了使用函数和可迭代对象作为 for
语句的目标;但是,Roberto 的设计要好得多,因为它只使用函数。在每次循环迭代中查找适当的 next
方法时,只在循环设置时调用一次适当的迭代器工厂,例如 pairs
。这当然更快,除非 next
方法查找很简单(通常不是)。由于被迭代的对象在整个循环中是恒定的,因此 next
查找将始终相同。但更重要的是,它也更通用,因为它不限制可迭代对象只有一种迭代机制。
我最初的提议解决的一个问题,而当前(5.1)Lua 实现中没有令人满意地解决的问题是,虽然对同一个对象拥有多个迭代机制很方便(string.gmatch
可能是最好的例子),但必须知道可迭代对象的类型才能编写正确的默认迭代机制,这一点很不方便。也就是说,人们(至少我)希望能够只写 for k, v in pairs(t) do
,而无需知道 t
的精确对象类型,在对象类型具有默认键/值类型迭代方法的情况下。
上面的代码将pairs
的实现泛化以咨询__pairs
元方法,这是以最简单的方式解决该问题的尝试。
不幸的是,ipairs
的泛化版本可能是不正确的,尽管为了历史完整性,我将其保留在代码中。通常,只有在对象的默认迭代机制应该是递增整数键的情况下,才需要覆盖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
__iter
元方法,它被通用for
结构直接使用,那么就可以为每个表设置合适的迭代器(或者在面向对象编程的类元表中继承)。在简单情况下,__iter
元方法将简单地设置为现有的pairs或ipairs函数,但也可以在适当的情况下使用自定义迭代器工厂。当然,这将涉及对语言定义的更改,而不仅仅是标准库。但是,这种实现将向后兼容,因为如果通用for
在该位置看到一个函数,它将像现在一样使用它并忽略元方法。如果它看到一个表或一个用户数据,它将查找__iter
元方法。
使用 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
__len
元方法
当 t
是一个表时,#t
不会调用 __len
元方法。请参阅 LuaList:2006-01/msg00158.html。这阻止了某些明显事物的简单实现,例如 ReadOnlyTables。
据报道,Lua 5.2 (LuaFiveTwo) 中实现了表的 __len
元方法。
Lua 中似乎存在一个普遍的原则,即元方法不会覆盖内置功能,而只是在会导致错误消息(或至少返回 nil
)的情况下提供功能。但是,表的 __len
必须是一个很好的例子,证明了“例外是规则的证明”,因为默认行为仅在连续数组的特殊情况下才有意义,而在其他情况下,可能会合理地预期其他行为。-- JohnHind
__index
是一个表时,重写 next()
'__iter'
元方法的补丁。