【问题标题】:How does the __call metamethod in Lua 5.1 actually work?Lua 5.1 中的 __call 元方法实际上是如何工作的?
【发布时间】:2011-08-28 05:36:50
【问题描述】:

作为练习,我正在尝试在 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

很明显,作为 set 表成员的插入函数正在显示。这不仅比原来的Set.insert(s, v) 问题更丑陋,而且还容易出现一些严重的问题(比如如果“插入”是某人试图输入的有效密钥会发生什么?)。又到了翻书的时候了。如果我尝试这样做会发生什么?:

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]: ?

在这一点上,我被难住了。我完全不知道从这里去哪里。我花了一个小时在这个代码上进行了不同程度的越来越绝望的变化,但最终结果是我没有任何工作。在这一点上,我忽略了什么无疑是显而易见的事情?

【问题讨论】:

  • 只想说我非常喜欢你的用户名

标签: lua call


【解决方案1】:

你说:

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

  • 当我调用 ts:insert(5) 时,insert 并没有 存在被调用意味着 ts 元表正在运行 要搜索“__call”。
  • ts 元表的“__call”键返回 Set.call。
  • 现在 Set.call 使用名称 insert 调用,这会导致 它返回 Set.insert 函数。
  • Set.insert(ts, 5) 被调用。

不,发生了什么:

  • insert 没有直接在ts 对象中找到时,Lua 会在其元表中查找__index
    • 如果它在那里并且是一个表,Lua 将在那里搜索insert
    • 如果它存在并且是一个函数,它将使用原始表(在这种情况下为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)
    -- Calls a function for every element in the set
    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) -- Calls getmetatable(ts).__call(ts, print),
          -- which means Set.call(ts, print)

-- The way __call and __index are set,
-- this is equivalent to the line above
ts:call(print)

【讨论】:

  • +1 用于实际解释原始代码的问题。
  • 顺便说一句,因为我把call(s, f)函数放在了Set——也就是getmetatable(ts).__index——你也可以写ts:call(print)
【解决方案2】:

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

  • 当我调用 ts:insert(5) 时,insert 不存在被调用的事实意味着将在 ts 元表中搜索“__call”。

你的问题。 __call 元方法在 table 本身 被调用(即作为一个函数)时被引用:

local ts = {}
local mt = {}

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

setmetatable(ts, mt)

ts() --> prints "Table called!"
ts(5) --> prints "Table called!" and 5
ts"String construct-call" --> prints "Table called!" and "String construct-call"

Lua 中面向对象的冒号调用,例如:

ts:insert(5)

只是语法糖

ts.insert(ts,5)

它本身就是语法糖

ts["insert"](ts,5)

因此,对ts 执行的操作不是调用,而是索引(@ 的结果 987654328@ 是所谓的),由__index 元方法管理。

__index 元方法可以是一个表,用于您希望索引“回退”到另一个表的简单情况(请注意,它是元表中的 __index 键的值被索引并且不是元表本身):

local fallback = {example = 5}
local mt = {__index = fallback}
local ts = setmetatable({}, mt)
print(ts.example) --> prints 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) --> prints "Indexing table named:" and "Bob",
              --> then on the next line "Example called!" and 5

有关元表的更多信息,请咨询the manual

【讨论】:

  • 这基本上是 Zecc 的答案颠倒了更多的中间示例代码。
  • 可能很好,但我被卖了。 :)
  • 不错。感谢您的指点和非常详细的回复。
  • 完整的答案,但对于像我这样的新手来说很容易理解。太棒了!
  • 美丽的解释;我想知道如何实现 JavaScript 风格的行为,其中函数是哈希表的子类型(因此可以使用通常的 table.key 语法访问属性)。 (更广泛的背景是我正在尝试创建类,我喜欢 JavaScript 的 Car.staticProperty 表示法,但更喜欢没有 new 关键字的 Python 风格的 Car() 构造函数表示法。) 这个答案向我展示了我可以简单地使用一个表格并给它一个__call 元方法来实现同样的效果!谢谢!
【解决方案3】:

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

Set = {}
Set.__index = Set 

function Set:new(collection)
  local o = {}
  for _, v in ipairs(collection) do
    o[v] = true
  end 
  setmetatable(o, self)
  return o
end

function Set:insert(v)
  self[v] = true
end

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

【讨论】:

    【解决方案4】:
    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
    

    【讨论】:

      猜你喜欢
      • 2012-12-08
      • 2023-03-25
      • 1970-01-01
      • 2021-01-21
      • 2011-09-27
      • 2021-12-16
      • 2013-03-14
      • 2021-03-23
      • 2011-02-11
      相关资源
      最近更新 更多