用 C 中的 __pairs 元方法迭代 table 的规范方法是什么?
What is the canonical way to iterate a table with a __pairs metamethod from C?
Lua 5.2 引入了 __pairs
(当然还有 __ipairs
)元方法。但是,lua_next()
似乎不支持它们,我认为这是有道理的。
是否有一种“正确”的方法可以使用内置的 C 函数在任一情况下(有或没有 __[i]pairs
元方法)很好地遍历 table 的键?
特别要求 5.4,但回到 5.2 的解决方案当然也很好。
据我所知,在撰写本文时,还没有真正“优雅”的方式来解决这个问题。
我能想到的最好办法是创建两个相同原型的闭包并直接与它们交互。这个答案主要集中在 __pairs
但我相信它可以简单地适应 __ipairs
.
首先是 next()
迭代器,它在裸 table 上工作(没有 __pairs
元方法)。它采用单个上值 - 目标 table - 并将调用转发给 lua_next()
.
static int next_iterator(lua_State *L) {
/* -1, +(2|0) */
/*
requires upvalues:
1: the table on which to call lua_next()
*/
return lua_next(L, lua_upvalueindex(1))
? 2
: 0;
}
然后我们有 __pairs()
迭代器,它将 table 作为上值 1,调用 __pairs()
的结果作为上值 2,并使用 [= 调用迭代器44=] 和当前调用的第一个参数(键)。
static int pair_iterator(lua_State *L) {
/* -1, +2 */
/*
requires upvalues:
1: the table on which to call the iterator
2: the iterator function (usually result
of a call to __pairs() metamethod)
*/
lua_pushvalue(L, lua_upvalueindex(1));
lua_pushvalue(L, -2);
lua_copy(L, lua_upvalueindex(2), -3);
lua_call(L, 2, 2);
return 2;
}
最后是迭代器函数。这意味着直接调用, 而不是 作为 lua_call()
等
的参数
它弹出迭代器函数(概念上,实际上不是)和键,并将迭代器和键/值压入堆栈。如果迭代结束,则两者都不会被压入堆栈 - 只是迭代器。
I make a point to say "pops . . . then pushes the iterator" as a point of documentation - treat this function just as you would lua_next()
. This also means you need to lua_pushnil(L)
for the first iteration, just like you would with lua_next()
.
static int iterator_next(lua_State *L) {
/* -2, +(1|3), r */
lua_pushvalue(L, -1);
lua_copy(L, -3, -2);
lua_call(L, 1, 2);
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
return 0;
}
return 1;
}
最后,我们可以利用一些 C 巫术为迭代创建一个基本干净的设置。这设置了 upvalues 和典型的 lua_next()
-like 迭代,初始“key”为 nil
.
/*
This assumes -1 has our table value
NOTE: This will also remove the table itself,
since we specify non-zero upvalue counts
in the call to `lua_pushcclosure()`.
This effectively replaces the table with
a valid iterator so that lua_pop() cleanly
cleans up the remaining artifacts after
iteration.
If you don't want to lose the table,
make sure to lua_pushvalue(L, -1) before
this switch statement.
*/
switch (luaL_getmetafield(L, -1, "__pairs")) {
default:
/* unsupported type; fall back to default */
lua_pop(L, 1);
/* fallthrough */
case LUA_TNIL:
/* nothing was pushed; no need to pop first. */
lua_pushcclosure(L, &next_iterator, 1);
break;
case LUA_TFUNCTION:
/* call the __pairs() metamethod and get a function back */
lua_pushvalue(L, -2);
lua_call(L, 1, 1);
/* now pass the pairs function iterator closure */
lua_pushcclosure(L, &pair_iterator, 2);
break;
}
/* Push `nil` as our first "key" */
lua_pushnil(L);
/* Iterate! */
while (iterator_next(L)) {
/*
-3 has our iterator function
-2 has the next key
-1 has the next value
*/
/* ... do something ... */
(void)0;
/*
Pop the value so that
the next call to iterator_next()
uses the current iteration's key.
*/
lua_pop(L, 1);
}
/* Finally, pop off the iterator. */
lua_pop(L, 1);
Lua 5.2 引入了 __pairs
(当然还有 __ipairs
)元方法。但是,lua_next()
似乎不支持它们,我认为这是有道理的。
是否有一种“正确”的方法可以使用内置的 C 函数在任一情况下(有或没有 __[i]pairs
元方法)很好地遍历 table 的键?
特别要求 5.4,但回到 5.2 的解决方案当然也很好。
据我所知,在撰写本文时,还没有真正“优雅”的方式来解决这个问题。
我能想到的最好办法是创建两个相同原型的闭包并直接与它们交互。这个答案主要集中在 __pairs
但我相信它可以简单地适应 __ipairs
.
首先是 next()
迭代器,它在裸 table 上工作(没有 __pairs
元方法)。它采用单个上值 - 目标 table - 并将调用转发给 lua_next()
.
static int next_iterator(lua_State *L) {
/* -1, +(2|0) */
/*
requires upvalues:
1: the table on which to call lua_next()
*/
return lua_next(L, lua_upvalueindex(1))
? 2
: 0;
}
然后我们有 __pairs()
迭代器,它将 table 作为上值 1,调用 __pairs()
的结果作为上值 2,并使用 [= 调用迭代器44=] 和当前调用的第一个参数(键)。
static int pair_iterator(lua_State *L) {
/* -1, +2 */
/*
requires upvalues:
1: the table on which to call the iterator
2: the iterator function (usually result
of a call to __pairs() metamethod)
*/
lua_pushvalue(L, lua_upvalueindex(1));
lua_pushvalue(L, -2);
lua_copy(L, lua_upvalueindex(2), -3);
lua_call(L, 2, 2);
return 2;
}
最后是迭代器函数。这意味着直接调用, 而不是 作为 lua_call()
等
它弹出迭代器函数(概念上,实际上不是)和键,并将迭代器和键/值压入堆栈。如果迭代结束,则两者都不会被压入堆栈 - 只是迭代器。
I make a point to say "pops . . . then pushes the iterator" as a point of documentation - treat this function just as you would
lua_next()
. This also means you need tolua_pushnil(L)
for the first iteration, just like you would withlua_next()
.
static int iterator_next(lua_State *L) {
/* -2, +(1|3), r */
lua_pushvalue(L, -1);
lua_copy(L, -3, -2);
lua_call(L, 1, 2);
if (lua_isnil(L, -2)) {
lua_pop(L, 2);
return 0;
}
return 1;
}
最后,我们可以利用一些 C 巫术为迭代创建一个基本干净的设置。这设置了 upvalues 和典型的 lua_next()
-like 迭代,初始“key”为 nil
.
/*
This assumes -1 has our table value
NOTE: This will also remove the table itself,
since we specify non-zero upvalue counts
in the call to `lua_pushcclosure()`.
This effectively replaces the table with
a valid iterator so that lua_pop() cleanly
cleans up the remaining artifacts after
iteration.
If you don't want to lose the table,
make sure to lua_pushvalue(L, -1) before
this switch statement.
*/
switch (luaL_getmetafield(L, -1, "__pairs")) {
default:
/* unsupported type; fall back to default */
lua_pop(L, 1);
/* fallthrough */
case LUA_TNIL:
/* nothing was pushed; no need to pop first. */
lua_pushcclosure(L, &next_iterator, 1);
break;
case LUA_TFUNCTION:
/* call the __pairs() metamethod and get a function back */
lua_pushvalue(L, -2);
lua_call(L, 1, 1);
/* now pass the pairs function iterator closure */
lua_pushcclosure(L, &pair_iterator, 2);
break;
}
/* Push `nil` as our first "key" */
lua_pushnil(L);
/* Iterate! */
while (iterator_next(L)) {
/*
-3 has our iterator function
-2 has the next key
-1 has the next value
*/
/* ... do something ... */
(void)0;
/*
Pop the value so that
the next call to iterator_next()
uses the current iteration's key.
*/
lua_pop(L, 1);
}
/* Finally, pop off the iterator. */
lua_pop(L, 1);