Lua - 为什么返回 C 函数作为 userdata?

我正在为我的引擎开发游戏脚本,并使用元表将来自表(存储玩家自定义函数和数据)的函数重定向到 userdata 对象上(这是我的 Player 类的主要实现),以便用户可以使用“self”同时引用两者。

这是我在 Player 类中在 C# 中绑定的方法:

        state.NewTable("Player");    // 创建 Player 包装表
        state["Player.data"] = this; // 将 Player.data 绑定到 Player 类
        state.NewTable("mt");        // 创建元表的临时表
        state.DoString(@"mt.__index = function(self,key)
                             local k = self.data[key]
                             if key == 'data' or not k then
                                 return rawget(self, key)
                             elseif type(k) ~= 'function' then
                                 print(type(k))
                                 print(k)
                                 return k
                             else
                                 return function(...)
                                     if self == ... then
                                         return k(self.data, select(2,...))
                                     else
                                         return k(...)
                                     end
                                 end
                             end
                         end");
        state.DoString("setmetatable(Player, mt)"); // 修改 Player 的元表

对于我的 Player 类,我实现了一个方法 bool IsCommandActive(string name)。当我需要使用 self 来调用此方法时,它需要使用 userdata 对象而不是表,否则我会收到以下错误消息:

NLua.Exceptions.LuaScriptException: 'instance method 'IsCommandActive' 需要一个非空目标对象'

这是显而易见的。这是因为 self 指向表而不是 userdata。因此,我实现了一个元表,这样它就可以使用 self 来引用任一者。这一实现取自这里,但以下是我的特定变体(我的 userdata 存储在一个名为 data 的索引中:

mt.__index = function(self,key)
    local k = self.data[key]
        if key == 'data' or not k then
            return rawget(self, key)
        elseif type(k) ~= 'function' then
            print(type(k))
            print(k)
            return k
        else
            return function(...)
                if self == ... then
                    return k(self.data, select(2,...))
                else
                    return k(...)
                end
            end
        end
    end
end

之后我使用 setmetatable,显然。

现在进入我的问题的实质。请注意下面的代码,即在 elseif 下打印 type(k)print(k)。这是因为我注意到我仍然得到相同的错误消息,所以我想进行一些调试。在这样做时,我得到了以下输出(我相信这是针对 IsCommandActive 的):

userdata:0BD47190

难道不应该打印'function'吗?为什么打印'userdata:0BD47190'?如果是这样,如何检测值是否为C函数,以便我可以做出正确的重定向?

点赞
用户2962027
用户2962027

经过大量阅读有关元表的文章,我终于解决了我的问题。

回答标题中的问题,显然 NLua 就是决定这样做并且实现特定的。在任何其他绑定中,它也可能会返回 function,但这显然不适用于 NLua。

至于我如何实现我想要的功能,我不得不定义元表 __index__newindex 函数:

        state.NewTable("Player");
        state["Player.data"] = this;
        state.NewTable("mt");
        state.DoString(@"mt.__index = function(self,key)
                             local k = self.data[key]
                             local metatable = getmetatable(k)
                             if key == 'data' or not k then
                                 return rawget(self, key)
                             elseif type(k) ~= 'function' and (metatable == nil or metatable.__call == nil) then
                                 return k
                             else
                                 return function(...)
                                     if self == ... then
                                         return k(self.data, select(2,...))
                                     else
                                         return k(...)
                                     end
                                 end
                             end
                         end");
        state.DoString(@"mt.__newindex = function(self, key, value)
                             local c = rawget(self, key, value)
                             if not c then
                                 local dataHasKey = self.data[key] ~= key
                                 if not dataHasKey then
                                     rawset(self, key, value)
                                 else
                                     self.data[key] = value
                                 end
                             else
                                 rawset(self, key, value)
                             end
                         end");
        state.DoString("setmetatable(Player, mt)");

__index 的作用是覆盖表的索引方式。在这个实现中,如果在 Player 包装表中没有找到 key,那么它会去尝试从 Player.data 中的 userdata 中检索它。如果它在那里不存在,那么 Lua 就会做它的事情并返回 nil

就这样,我就可以从 userdata 中检索字段了!然而,我很快开始注意到,如果我在 Lua 中设置了 self.Pos,那么 Player.Pos 在支持的 C# 代码中就不会更新。我很快意识到,这是因为 PosPlayer 包装表中生成了一个错误的索引,这意味着它实际上并不存在!

这不是预期的行为,因此我还必须覆盖 __newindex。在这个特定的实现中,它检查 Player.data (userdata) 是否有 key,如果有,则为该特定 key 设置数据。如果它在 userdata 中不存在,则应为 Player 包装表创建它,因为它应该是用户自定义的 Player 实现的一部分。

2018-03-11 17:53:30
用户5675002
用户5675002

任何属于 C 类的函数或对象都是用户数据 userdata

这并不正确。无论是本地函数还是 Lua 编写的函数,函数都是函数。检查本地函数的类型会打印出 "function"。

只是你的绑定解决方案可能使用了一个带有 __call 元方法的 userdata,以公开与其相关联的某些状态/上下文的编码器。但这并不意味着每个本地函数都是一个 userdata,或者每个绑定库都会以相同的方式实现。可以使用 Lua table 相同的方式有效地实现。那么你会说 "每个本地函数都是一个表格" 吗? :)

2018-03-11 19:03:13