Lua 5.1 中的 __call 元方法实际上是如何工作的?

我正在尝试在 Lua 中创建一个集合实现作为练习。具体来说,我想要采取 Pil2 11.5 中简单的集合实现,并扩展它以包括插入值、删除值等功能。

现在要做的显而易见的方法(以及有效的方法)是这样的:

Set = {}
function Set.new(l)
    local s = {}
    for _, v in ipairs(l) do
        s[v] = true
    end
    return s
end
function Set.insert(s, v)
    s[v] = true
end

ts = Set.new {1,2,3,4,5}
Set.insert(ts, 5)
Set.insert(ts, 6)

for k in pairs(ts) do
    print(k)
end

如预期的那样,我得到数字 1 到 6 的输出。但是那些对 Set.insert(s, value) 的调用风格真的相当丑陋。我更愿意能够调用类似于 ts:insert(value) 这样的东西。

我第一次尝试解决这个问题的解决方案看起来像这样:

Set = {}
function Set.new(l)
    local s = {
        insert = function(t, v)
            t[v] = true
        end
    }
    for _, v in ipairs(l) do
        s[v] = true
    end
    return s
end

ts = Set.new {1,2,3,4,5}
ts:insert(5)
ts:insert(6)

for k in pairs(ts) do
    print(k)
end

这基本上很好,直到你看到它的输出:

1
2
3
4
5
6
insert

很明显,集合表的成员 insert 函数正在被显示。这甚至比原来的 Set.insert(s, v) 问题更丑陋,而且也很容易出现一些严重的问题(例如,如果 "insert" 是某人试图输入的有效键,会发生什么问题?)。是时候重新看一下书了。如果我尝试这个怎么样呢?:

Set = {}
function Set.new(l)
    local s = {}
    setmetatable(s, {__call = Set.call})
    for _, v in ipairs(l) do
        s[v] = true
    end
    return s
end
function Set.call(f)
    return Set[f]
end
function Set.insert(t, v)
    t[v] = true
end

ts = Set.new {1,2,3,4,5}
ts:insert(5)
ts:insert(6)

for k in pairs(ts) do
    print(k)
end

现在我正在阅读这段代码:

  • 当我调用 ts:insert(5) 时,insert 不存在可以调用的事实意味着 ts 元表会被搜索 "__call"。
  • ts 元表的 "__call" 键返回 Set.call
  • 现在调用 Set.call 并使用名称 insert,这会导致返回 Set.insert 函数。
  • 调用 Set.insert(ts, 5)

实际发生的事情是这样的:

lua: xasm.lua:26: attempt to call method 'insert' (a nil value)
stack traceback:
        xasm.lua:26: in main chunk
        [C]: ?

此时,我感到困惑。我完全不知道接下来该做什么。我随机尝试了一个小时以上,试图在这段代码上进行细微调整,但最终的结果是没有可以正常工作的代码。我在这一点上肯定忽略了显而易见的事情是什么?

原文链接 https://stackoverflow.com/questions/6048118

点赞
stackoverflow用户33252
stackoverflow用户33252
Set = {}
function Set.new(l)
    local s = {}
    setmetatable(s, {__index=Set})
    for _, v in ipairs(l) do
        s[v] = true
    end
    return s
end
function Set.call(f)
    return Set[f]
end
function Set.insert(t, v)
    t[v] = true
end

ts = Set.new {1,2,3,4,5}
ts:insert(5)
ts:insert(6)

for k in pairs(ts) do
    print(k)
end
Set = {}
-- 新建集合
function Set.new(l)
    local s = {}
    setmetatable(s, {__index=Set})
    for _, v in ipairs(l) do
        s[v] = true
    end
    return s
end
-- 调用 Set 对象的方法
function Set.call(f)
    return Set[f]
end
-- 插入元素到集合中
function Set.insert(t, v)
    t[v] = true
end

ts = Set.new {1,2,3,4,5} -- 新建集合
ts:insert(5) -- 插入元素 5
ts:insert(6) -- 插入元素 6

for k in pairs(ts) do -- 遍历集合
    print(k)
end
2011-05-18 17:58:11
stackoverflow用户206020
stackoverflow用户206020
Set = {}
Set.__index = Set

-- 新建一个 Set 对象
function Set:new(collection)
  local o = {}
  for _, v in ipairs(collection) do
    o[v] = true
  end
  setmetatable(o, self)
  return o
end

-- 向 Set 中插入元素
function Set:insert(v)
  self[v] = true
end

-- 创建一个 Set 对象
set = Set:new({1,2,3,4,5})
print(set[1]) --> true
print(set[10]) --> nil
set:insert(10)
print(set[10]) --> true

我修改了你的第一个版本,这个版本将提供我认为你正在寻找的功能。

2011-05-18 17:59:04
stackoverflow用户400127
stackoverflow用户400127

你说:

现在我读这段代码的方式是:

  • 当我调用 ts:insert(5) 时,由于 insert 并不存在于 ts 中,所以 lua 会在 ts 的元表中查找 "__call"。
  • ts 的元表中的 "__call" 键返回 Set.call。
  • 现在会调用 Set.call 方法并以 insert 为名称,导致返回 Set.insert 函数。
  • 调用 Set.insert(ts, 5) 。

不是这样的,实际上是这样的:

  • ts 对象中不存在 insert 时,Lua 会查找它的元表对象的 __index 键值对。

    • 如果有 __index 并且它是一个表,Lua 会在其中查找 insert
    • 如果有 __index 并且它是一个函数,Lua 会用原始的表(即 ts)以及被查找的键值(即 insert)作为参数来调用它。
    • 如果没有,就会被认为是 nil

你遇到的错误是因为你的元表中没有设置 __index,所以你实际上是在调用一个 nil 值。

如果你将要将方法存储在其中的话,可以通过将 __index 指向某个表(例如 Set)来解决这个问题。

至于 __call,它用于当你把对象作为函数来调用时。例如:

Set = {}
function Set.new(l)
    local s = {}
    setmetatable(s, {__index=Set, __call=Set.call})
    for _, v in ipairs(l) do
        s[v] = true
    end
    return s
end
function Set.call(s, f)
    -- 收集集合中的每个元素,并每个元素调用一个函数
    for k in pairs(s) do
        f(k)
    end
end
function Set.insert(t, v)
    t[v] = true
end

ts = Set.new {1,2,3,4,5}
ts:insert(5)
ts:insert(6)

ts(print) -- 等价于 getmetatable(ts).__call(ts, print),即 Set.call(ts, print)

-- 根据 __call 和 __index 的设置,
-- 这个调用等价于上面的一行
ts:call(print)
2011-05-18 19:25:58
stackoverflow用户34799
stackoverflow用户34799

现在我读这段代码的方式是:

  • 当我调用ts:insert(5), 由于insert不存在被调用,这意味着ts元表将被搜索以寻找"__call"。

你的问题在这里。当调用表本身(即作为函数)时,会查询“__call”元方法:

local ts = {}
local mt = {}

function mt.__call(...)
    print("Table called!", ...)
end

setmetatable(ts, mt)

ts() --> 打印“Table called!”
ts(5) --> 打印“Table called!”和5
ts“String construct-call” --> 打印“Table called!”和“String construct-call”

Lua中的面向对象冒号调用,如下所示:

ts:insert(5)

只是语法上的糖,表示为

ts.insert(ts,5)

它本身就是语法糖

ts [“insert”](ts,5

因此,在ts上采取的操作不是调用,而是索引ts [“insert”]的结果被调用),其受__index元方法的控制。

对于简单的情况,__index元方法可以是一个表,其中索引会“回退”到另一个表(请注意,它是元表中__index键的_value_被索引,_而不是元表本身):

local fallback = {example = 5}
local mt = {__index = fallback}
local ts = setmetatable({}, mt)
print(ts.example) --> 打印 5

作为函数的__index元方法类似于您预期的带有Set.call的签名,只是它在键之前将被索引的表传递:

local ff = {}
local mt = {}

function ff.example(...)
  print("Example called!",...)
end

function mt.__index(s,k)
  print("Indexing table named:", s.name)
  return ff[k]
end

local ts = {name = "Bob"}
setmetatable(ts, mt)
ts.example(5) --> 打印“索引表命名:”和“Bob”,
              --> 然后在下一行打印“Example called!”和5

有关元表的更多信息,请参见手册

2011-05-18 20:45:22