泛化對和 ipairs

lua-users home
wiki

Lua 5.2+ __pairs 和 __ipairs

Lua 5.2 針對在 for 迴圈中控制表的 pairs 和 ipairs 行為,推出了 __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       !

SteveDonovan

在 Lua 5.1 實作自訂迴圈行為

Lua 中的 Table 具有下列必要屬性(以及其他屬性)

第一個屬性可透過 tables 和 userdata 的 __index 和 __newindex metamethods 來自訂。在 Lua 5.2 之前,沒有直接的方法可以自訂第二和第三個屬性(LuaVirtualization)。我們在此探討自訂這些屬性的方式。

定義 __next 和 __index metamethods

如果你僅對重寫 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 metamethod

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 metamethods

以下 C 實作提供類似的行為,但使用 __pairs__index metamethods。這種使用 __pairs metamethod 的方式可能比以上使用 __next metamethod 的另一種方式更快。

此程式碼重新定義 pairsipairs,以便

它應該為字串安裝 __pairs 方法,但目前尚未安裝。請隨時新增,所有片段都存在。

它預期會使用 require "xt" 載入,而且會產生包含各種函數的 "xt" table。不過,它也會覆寫全域 table 中的 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;
}

這裡有一個以 Lua 撰寫的替代函式,用來取代 pairs。注意:與 C 的實作不同,若元表受到保護,這個 Lua 實作將無法使用。

local _p = pairs; function pairs(t, ...)
  return (getmetatable(t).__pairs or _p)(t, ...) end

-- RiciLake

定義 __next 和 __ipairs 元方法

Beginning Lua Programming 的 262 頁使用類似的方法,但改用 __next__ipairs 元方法。可以在 [1] 的下載連結中找到它的程式碼(請參閱 Chapter 08/orderedtbl.lua)。但請注意,該程式碼需要更正(記載於 306 頁),讓 {__mode = "k"} 成為 RealTblsNumToKeysKeyToNums 表的元表。

以下是 RiciLake 撰寫的另一個 OrderedTable 實作。

更多備註

如果您真的想要仿效這樣的行為,您需要修改 Lua 原始程式碼以加入 lua_rawnext(),並更新 lua_next(),若為這種狀況,請參閱 RiciLake 撰寫的 ExtendingForAndNext 條目。

歷史備註:我(RiciLake)在 2001 年 9 月撰寫了 ExtendingForAndNext,當時 Lua 尚未具備通用的 for 陳述式。該設計係根據 Lua 4 中現有的程式碼而來,我希望它對通用 for 陳述式的設計產生了影響,後者在幾個月後便出現了。當時,Lua 使用「標記方法」,而非元方法;標記方法的存取速度會比元方法略快(經過最佳化則例外),但很明顯的是,元方法更好。我僅提出這一點來說明 ExtendingForAndNext 修補程式脈絡中的一些觀念。

ExtendingForAndNext 設想將函式和可迭代物件視為 for 陳述式的目標;然而,Roberto 的設計好上許多,因為它只使用函式。每次迴圈迭代都不會查詢適當的 next 方法,適當的迭代器工廠(例如 pairs)在迴圈設定時只會呼叫一次。這樣做肯定比較快,除非 next 方法的查詢是微不足道的(通常並非如此)。由於迭代的物件在迴圈中是不變的,next 查詢永遠會是一樣的。但更重要的是,它也比較通用,因為它不會限制可迭代物件只能有一種迭代機制。

我原始提案所處理、而且在目前的 (5.1) Lua 實作中尚未圓滿解決的問題是,雖然對同一個物件有多種迭代機制很方便(string.gmatch 可能就是最好的例子),但要編寫正確的預設迭代機制時,知道一個可迭代物件的類型卻不方便。也就是說,(至少我)想只要寫 for k, v in pairs(t) do,而不必知道 t 的精確物件類型,而且物件類型有預設的 key/value 類型迭代方法。

上面的程式碼將 pairs 的實作一般化以諮詢 __pairs 元方法,是嘗試以最簡單的方式解決那個問題。

遺憾的是,ipairs 的一般化版本可能不正確,不過因為歷史完整性的關係,我把它留在程式碼中了。通常,你會希望在物件的預設迭代機制應該是遞增整數 key 的情況下,覆寫 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。但如果有一個由一般的 for 結構直接使用的 __iter 元方法,就可以針對每個表格設定合適的迭代器(或繼承至面向物件程式設計的類元表格)。在簡單的案例中,__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

據報導,表格的 __len 元方法已經在 Lua 5.2 中實作(LuaFiveTwo)。

Lua 有一個通則,元方法不會覆寫內建函數,只會在原本會產生錯誤訊息的情況下(或至少會傳回 nil)提供函數。但是表格的 __len 應該是「證明規則的例外」的合理案例,因為預設行為只適用於連續陣列的特殊情況,可以合理預期在其他情況下會有不同的行為。-- JohnHind

另請參閱


RecentChanges · 偏好設定
編輯 · 歷程
最後編輯時間:2020年2月13日 下午12:25 GMT (比較版本)