泛型对和 Ipairs

lua-users home
wiki

Lua 5.2+ __pairs 和 __ipairs

Lua 5.2 引入了 __pairs__ipairs 方法,用于控制 for 循环中表对 pairsipairs 的行为。__pairs__ipairs 方法的工作方式类似于标准迭代器方法。以下是一个无状态迭代器的示例,其行为类似于默认的 pairsipairs 迭代器,但可以被覆盖以获得循环行为,例如过滤掉值或以不同的顺序循环遍历项目。

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       !

SteveDonovan

在 Lua 5.1 中实现自定义循环行为

Lua 中的表具有以下基本属性(以及其他属性)

第一个属性可以通过表和用户数据的 __index__newindex 元方法进行自定义。在 Lua 5.2 之前,没有直接的方法来自定义第二个和第三个属性(LuaVirtualization)。我们在这里研究自定义这些属性的方法。

定义 __next 和 __index 元方法

如果您只希望重写 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

-- PeterHillDavidManura

定义 __pairs 和 __index 元方法

以下 C 实现提供了类似的行为,但使用 __pairs__index 元方法。这种使用 __pairs 元方法的方法可能比上面使用 __next 元方法的替代方法更快。

此代码重新定义了 pairsipairs,以便

它应该为字符串安装一个 __pairs 方法,但它还没有。请随时添加它,所有部分都在那里。

它期望使用 require "xt" 加载,并将生成一个带有各种函数的 "xt" 表。但是,它还会覆盖全局表中的 pairsipairs。如果您发现这令人反感,请从 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

定义 __next 和 __ipairs 元方法

Lua 编程入门 第 262 页使用类似的方法,但使用 __next__ipairs 元方法。代码可以通过 [1] 中的下载链接在线获取(参见第 08 章/orderedtbl.lua)。但是,请注意,此代码需要进行修正(在第 306 页中指出),以使 {__mode = "k"} 成为 RealTblsNumToKeysKeyToNums 表的元表。

以下是 RiciLake 提供的另一种 有序表 实现。

更多说明

如果您想真正模拟这种行为,您需要修改 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
问题在于,适合每种类型表的迭代器各不相同,例如,对于类似数组的表使用ipairs,对于类似字典的表使用pairs。但是,如果有一个__iter元方法,它被通用for结构直接使用,那么就可以为每个表设置合适的迭代器(或者在面向对象编程的类元表中继承)。在简单情况下,__iter元方法将简单地设置为现有的pairsipairs函数,但也可以在适当的情况下使用自定义迭代器工厂。当然,这将涉及对语言定义的更改,而不仅仅是标准库。但是,这种实现将向后兼容,因为如果通用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

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2020 年 2 月 13 日下午 12:25 GMT (差异)