首页 > 解决方案 > 用 C 中的 __pairs 元方法迭代表的规范方法是什么?

问题描述

Lua 5.2 引入了__pairs(当然还有__ipairs)元方法。但是,lua_next()似乎并不支持它们,我认为这是有道理的。

是否有一种“正确”的方法可以__[i]pairs使用内置的 C 函数在任何一种情况下(使用或不使用元方法)很好地遍历表的键?

当然,专门要求 5.4 但回到 5.2 的解决方案也很好。

标签: clua

解决方案


据我所知,在撰写本文时,还没有真正的“优雅”方式来解决这个问题。

我能想到的最好的办法是创建两个相同原型的闭包并直接与它们交互。这个答案主要集中在__pairs但我相信它可以很容易地适用于__ipairs.

首先是next()在裸表上工作的迭代器(没有__pairs元方法)。它采用单个上值 - 目标表 - 并将调用转发到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()迭代器,它将表作为 upvalue 1 并将调用的结果__pairs()作为 upvalue 2 并使用表和当前调用的第一个参数(键)调用迭代器。

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()等人的参数。

它弹出迭代器函数(概念上,实际上不是)和键,并将迭代器和键/值压入堆栈。如果迭代已经结束,那么两者都不会被压入堆栈 - 只是迭代器。

我强调说“pops . . . then pushes the iterator”作为文档的一个要点——像对待这个函数一样对待这个函数lua_next()。这也意味着您需要lua_pushnil(L)进行第一次迭代,就像使用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()迭代,初始“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);

推荐阅读