用 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);