你想做什么
是
有可能,但不是以你想要的方式。
我认为有必要回顾一下方法调用和元表/元方法是如何工作的,以及您编写的代码实际上在做什么。tl;博士是:
-
方法调用只是普通的字段查找
-
元表及其包含的元方法是
运算符重载
,而不是方法定义
-
如果要为userdata实现此功能,则需要
__index
可以处理字段和方法查找的元方法
首先,Lua在“方法”和“字段”之间没有内在的区别。在组织代码时,您可能会发现区分这两种语言很方便,但就Lua语言而言
方法和字段是一样的
方法只是一个字段,其中键是有效的lua标识符,值是函数。
所以,当你写下
me:AddCollisionObjHook(playerHitObj)
,实际发生的事情是这样的:
local self = me
local method = self["AddCollisionObjHook"]
method(self, playerHitObj)
(关于这一点,请注意两点:
-
没有真正的新本地人被创造出来;这一切都发生在Lua解释器的内部。
-
self["AddCollisionObjHook"]
和
self.AddCollisionObjHook
是两种书写同一事物的方式;后者只是前者的捷径。)
那么,这是怎么回事
self[“AddCollisionObjHook”]
查找工作?与任何其他字段查找的工作方式相同。Lua手册对此进行了详细介绍,包括伪代码,但与您的代码相关的部分是:
-- We're looking up self[key] but self is userdata, not table
local mt = getmetatable(self)
if mt and type(mt.__index) == 'function' then
-- user provided an __index function
return mt.__index(self, key)
elseif mt and mt.__index ~= nil then
-- user provided an __index table (or table-like object)
-- retry the lookup using it
return mt.__index[key]
else
-- no metatable, or metatable lacks __index metamethod
error(...) -- don't know how to do field lookup on this type!
end
注意
在这一过程中,除了
__索引
在元表中查找
.元表存在
只有
告诉Lua如何为通常没有运算符的类型实现运算符;在本例中,字段查找(“索引”)运算符(
[]
,以及它的别名
.
和
:
)对于特定类型的用户数据。这完全取决于
__索引
它本身可以处理将字段名转换为值的实际过程,可以是一个可以重试查找的表,也可以是一个可以返回相关值的函数。
因此,这就为我们提供了如何支持(可设置)字段和(可调用)方法的答案:
-
__newindex
需要了解如何设置字段
-
__索引
需要了解如何返回
二者都
字段值
和
方法实现
因为,从Lua的角度来看,字段查找和方法查找都是相同的操作,因此
__索引
两者都习惯了。
有鉴于此,我们应该如何组织代码来支持这两个方面,以及如何重新构造代码以使其工作?有很多方法可以做到这一点,不过为了回答这个问题,我将做一些假设:
-
领域
全部存储在C端,没有相应的数据需要在Lua中管理
-
方法
不能被Lua代码覆盖
-
元方法
与单独存储
实例方法
最后一个不是绝对必要的;事实上,在同一个表中同时存储元方法和实例方法是很常见的(我通常自己做)。然而,我认为这也会让Lua新手对他们之间的区别产生困惑,所以为了让代码尽可能清晰,我在这个答案中将他们分开。
考虑到这一点,让我们重新编写设置代码。我看过你的编辑,试图重建你最初的想法。
static int playerget(lua_State *L)
{
Player *player = *CHECKPLAYER(L, 1);
const char *field = luaL_checkstring(L, 2);
// Check if it's a method, by getting the method table
// and then seeing if the key exists in it.
// This code can be re-used (or factored out into its own function)
// at the start of playerset() to raise an error if the lua code tries
// to overwrite a method.
lua_getfield(L, LUA_REGISTRYINDEX, "player-methods");
lua_getfield(L, -1, field);
if (!lua_isnil(L, -1)) {
// Lookup in methods table successful, so return the method impl, which
// is now on top of the stack
return 1;
} else {
// No method, so clean up the stack of both the nil value and the
// table of methods we got it from.
lua_pop(L, 2);
}
if (!strcmp(field, "next"))
// ... code for reading fields rather than methods goes here ... //
}
// Functions that are part of the player library rather than tied to any
// one player instance.
static const struct luaL_Reg playerlib_api[] = {
// player.head() -> returns the first player
{"head", player_head},
{NULL, NULL}
};
// Metamethods defining legal operators on player-type objects.
static const struct luaL_Reg playerlib_metamethods[] = {
// Overrides the tostring() library function
{"__tostring", player2string},
// Adds support for the table read operators:
// t[k], t.k, and t:k(...)
{"__index", playerget},
// Adds support for the table write operators:
// t[k]=v and t.k=v
{"__newindex", playerset},
{NULL, NULL}
};
// Instance methods for player-type objects.
static const struct luaL_Reg playerlib_methods[] = {
// player_obj:AddCollisionObjHook(hook)
{"AddCollisionObjHook", AddCollisionObjHook},
// player_obj:AddPreThinker(thinker)
{"AddPreThinker", AddPreThinker},
// player_obj:AddPostThinker(thinker)
{"AddPostThinker", AddPostThinker},
{NULL, NULL}
};
int Lua_PlayerLib(lua_State *L)
{
// Create the metatable and fill it with the stuff from playerlib_metamethods.
// Every time a player object is pushed into Lua (via player_head() or similar)
// this metatable will get attached to it, allowing lua to see the __index,
// __newindex, and __tostring metamethods for it.
luaL_newmetatable(L, "player");
luaL_setfuncs(L, playerlib_metamethods, 0);
lua_pop(L, 1);
// Create the method table and fill it.
// We push the key we're going to be storing it in the registry under,
// then the table itself, then store it into the registry.
lua_pushliteral(L, "player-methods");
luaL_newlib(L, playerlib_methods, 0);
lua_settable(L, LUA_REGISTRYINDEX);
// Initialize the `player` library with the API functions.
luaL_newlib(L, playerlib_api, 0);
// Set that table as the value of the global "player".
// This also pops it, so we duplicate it first...
lua_pushvalue(L, -1);
lua_setglobal(L, "player");
// ...so that we can also return it, so that constructs like
// local player = require 'player'; work properly.
return 1;
}
分解一下,我们可以得到三张表:
-
player
,其中包含实际的库API,如
player.head()
-
REGISTRY["player"]
,它保存所有玩家对象共享的元表
-
__tostring
调用它进行预打印
-
__新索引
为字段写入调用
-
__索引
为字段读取调用(包括方法查找!)
-
REGISTRY["player-methods"]
,其中包含所有实例方法
如上所述,我将元方法表和方法表分开,希望尽量减少概念上的混淆;惯用代码可能会将所有方法和元方法存储在一起,并使用
luaL_getmetafield()
一开始
playerset()
和
playerget()
进行方法查找。