

深入Lua:元表
source link: https://zhuanlan.zhihu.com/p/98072589
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入Lua:元表
我觉得Lua最强大的地方在于对象可以设置元表,而元表会影响对象的访问行为。
Table的结构有一个metatable成员,userdata类型的结构也有一个metatable成员,这表明Table和userdata对象可以单独设置元表,其他每种类型的元素是共享的。通常情况下,我们只会对Table或Userdata设置元表,其他类型没有办法通过Lua代码设置元表。
字符串库默认给string类型设置了一个元表,使得它可以像对象一样调用字符串库的函数,比如"hello":len()
设置元表的API为lua_setmetatable
:
LUA_API int lua_setmetatable (lua_State *L, int objindex) {
TValue *obj;
Table *mt;
lua_lock(L);
// 从栈中取对象
obj = index2addr(L, objindex);
// 从栈中取元表
if (ttisnil(L->top - 1))
mt = NULL;
else {
api_check(L, ttistable(L->top - 1), "table expected");
mt = hvalue(L->top - 1);
}
// 根据不同类型设置元表
switch (ttnov(obj)) {
case LUA_TTABLE: { // table
hvalue(obj)->metatable = mt;
if (mt) {
luaC_objbarrier(L, gcvalue(obj), mt);
luaC_checkfinalizer(L, gcvalue(obj), mt);
}
break;
}
case LUA_TUSERDATA: { // userdata
uvalue(obj)->metatable = mt;
if (mt) {
luaC_objbarrier(L, uvalue(obj), mt);
luaC_checkfinalizer(L, gcvalue(obj), mt);
}
break;
}
default: { // 其他类型
G(L)->mt[ttnov(obj)] = mt;
break;
}
}
L->top--;
lua_unlock(L);
return 1;
}
元表是通过定义在其上的字段来影响对象的行为的,这些字段也称为元方法,但其实不是每个字段都是函数,有些是字符串,有些甚至可以另外一个表,大概列举如下:
__tostring
, __pairs
, __name
, __index
, __newindex
, __call
, __add
, __sub
, __mul
, __div
, __mod
, __pow
, __unm
, __idiv
, __band
, __bor
, __bxor
, __bnot
, __shl
, __shr
, __concat
, __len
, __eq
, __lt
, __le
, __gc
, __mode
,
关于元方法的具体含义,具体请看文档,这里不再描述。
在ltm.h|.c
中实现了元方法的一些辅助函数,其中有一个枚举:
typedef enum {
TM_INDEX,
TM_NEWINDEX,
TM_GC,
TM_MODE,
TM_LEN,
TM_EQ, /* 在这之前的元方法可以快速访问 */
...
TM_N /* 枚举数量 */
} TMS;
枚举了元方法的类型,在global_state中有一个tmname字段,为TMS到元方法名字的映射,在下面代码初始化:
void luaT_init (lua_State *L) {
static const char *const luaT_eventname[] = { /* ORDER TM */
"__index", "__newindex",
"__gc", "__mode", "__len", "__eq",
"__add", "__sub", "__mul", "__mod", "__pow",
"__div", "__idiv",
"__band", "__bor", "__bxor", "__shl", "__shr",
"__unm", "__bnot", "__lt", "__le",
"__concat", "__call"
};
int i;
for (i=0; i<TM_N; i++) {
G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]);
luaC_fix(L, obj2gco(G(L)->tmname[i])); /* never collect these names */
}
}
由上面代码可知tmname的字段为不会被GC回收的短字符串对象。这样的话使用TMS宏即可快速从tmname找到相应的元方法名。再由元方法名找元表的字段值。
快速访问元方法
正常取元方法值是下面这个函数:
const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, TMS event) {
// 主体逻辑很简单,根据类型取出元表,然后调用luaH_getshortstr取元方法
Table *mt;
switch (ttnov(o)) {
case LUA_TTABLE:
mt = hvalue(o)->metatable;
break;
case LUA_TUSERDATA:
mt = uvalue(o)->metatable;
break;
default:
mt = G(L)->mt[ttnov(o)];
}
return (mt ? luaH_getshortstr(mt, G(L)->tmname[event]) : luaO_nilobject);
}
ltm.h中有一个fasttm
宏,用于加速取表中某些元方法的值,看最上面的枚举,在TM_EQ之上那些元方法会用加速的方式,怎么加速呢:
// l为lua_State, et为元表,e为TMS枚举
#define fasttm(l,et,e) gfasttm(G(l), et, e)
// 先通过Table的flags标记位判断,如果该位存在,表示没有该元方法,直接返回NULL
// 否则才通过luaT_gettm去取
#define gfasttm(g,et,e) ((et) == NULL ? NULL : \
((et)->flags & (1u<<(e))) ? NULL : luaT_gettm(et, e, (g)->tmname[e]))
luaT_gettm代码如下:
const TValue *luaT_gettm (Table *events, TMS event, TString *ename) {
// 先尝试从Table中取元方法值
const TValue *tm = luaH_getshortstr(events, ename);
lua_assert(event <= TM_EQ);
// 如果值不存在,设置表中的flags相应位为1
// 下次再调用fasttm,就不会调到这个函数了
if (ttisnil(tm)) { /* no tag method? */
events->flags |= cast_byte(1u<<event); /* cache this fact */
return NULL;
}
else return tm;
}
我们前面看Table结构时,注意到这个flags成员,现在终于知道它是用在这里的。flags的位为1表示该位的元方法不存在,这样就避免每次查询元方法都要从元表去取,某些元方法的查询是很频繁的
但既然有了状态,就得维护这个状态,ltable.h有一个宏invalidateTMcache
,作用是把flags清0:
#define invalidateTMcache(t) ((t)->flags = 0)
注意fasttm只能用于表,用户数据只能通过luaT_gettmbyobj
查询元方法。
列举元方法的调用
不同的元方法会在不同的代码出现,下面只能列举一些。
__index
索引表,流程大概是这样的:
- 调用
luaV_gettable
取表的字段,其中: - 调用
luaV_fastget
取字段,如果失败则调用luaV_finishget
,这里面就会使用元方法 - 通过
fasttm
或luaT_gettmbyobj
得到元方法后,判断它是否为函数,如果为函数则调用luaT_callTM
,否则它应该是一个表,则继续这个过程。 - 假如一直这个循环,直到
MAXTAGLOOP
次,则Lua直接报错,说明这个__index链太长了。
具体逻辑请看上面这几个函数。
__newindex
设置表,流程大概是这样的:
- 调用
luaV_settable
设置表字段,其中: - 调用
luaV_fastset
设置字段,如果失败则调用luaV_finishset
,这里面就会使用元方法。 - 通过
fasttm
或luaT_gettmbyobj
得到元方法后,判断它是否为函数,如果为函数则调用luaT_callTM
,否则它应该是一个表,则继续这个过程。 - 假如一直这个循环,直到
MAXTAGLOOP
次,则Lua直接报错,说明这个__newindex链太长了。
具体逻辑请看上面这几个函数。
比较两个对象是否相等,在luaV_equalobj
中如果对象是表或用户数据,则尝试通过fasttm
取得它们的元方法,如果取得到,就调用luaT_callTM
。
__len
取对象长度,在luaV_objlen
函数中,中如果对象是表或用户数据,则尝试通过fasttm
取得它们的元方法,如果取得到,就调用luaT_callTM
。
__name
自定义对象的名字,luaT_objtypename
函数返回一个对象的名字,比如boolean, nil, table等等,但对象的名字可以自定义,只要这个对象有一个元表,指定元表的__name元方法即可,对象名字一般用于错误提示上,比如luaG_typeerror
等函数。
__tostring
返回对象的字符串表现,看一下Lua的tostring函数的调用链:
tostring -> luaB_tostring -> luaL_tolstring
luaL_tolstring尝试调用对象元表的tostring,如果没有tostring就根据类型来,最后默认就是typename : 对象地址
__pairs
遍历表:luaB_pairs->pairsmeta,先判断有没有元表,元表有没有存在__pairs,有就调用它,没有就调用luaB_next。
Recommend
-
16
作者:nicochen,腾讯 IEG 游戏开发...
-
18
深入Lua:垃圾回收3作为垃圾回收的最后一篇,要来描述一下内存如何统计,什么时候触发GC,以后回收的灵敏度等问题。看了这一章,你应该能够知道如何通过pause和step multiplier这两个参数来控制GC的速度。glo...
-
19
深入Lua:垃圾回收2一份参考文档关于Lua的GC的演进,有一份非常好的幻灯片,是之前看云风文章的时候发现的。文中讲了5.0以前基于标记...
-
24
深入Lua:垃圾回收标记和清扫Lua是一门自动内存管理的语言,它使用的是经典的标记和清扫算法。这个算法的原理其实非常简单,在我们的编程实践中或多或少都有类似的做法,一个理想的实现是这样的:明确对象模型和关系:...
-
4
深入Lua:调用相关的指令这一节我们来深入解析与调用相关的指令,这些指令是:OP_CALL 调用OP_TAILCALL 尾调用OP_VARARG 可变参数OP_RETURN 返回解析这些指令的过程中,最重要的是时刻跟踪栈的变...
-
20
深入Lua:调试设施Lua语言内部提供了完善的Debug库支持,使用它来实现一个Lua调试器并不是特别难。这一篇打算将其核心框架介绍一下。再看函数原型的数据调试的首要条件是要有调试信息,Lua的函数原型就带有丰富的调试信息,在预编译文...
-
12
深入Lua:协程的实现协程几乎是Lua代码中最复杂的逻辑,它是对Lua线程的封装,一个协程有自己的栈和调用链,所以协程之间的协作要考虑栈之间的数据交换,发生错误之后的栈恢复等等。本来upvalue也是一个棘手的问题,但因为upvalue是间接引用,...
-
12
深入Lua:保护模式的实现longjmp机制Lua在底层会根据语言选择不同的异常机制,在C++下用try.. catch,在C下则用longjmp。用下面的宏把差异封装起来:#if defined(__cplusplus) && !defined(LUA_USE...
-
15
深入Lua:函数和闭包2Lua文件的加载Lua是以函数为编译单元的,一个Lua文件加载进来后就是一个函数,定义在Lua文件中的顶层本地变量,和普通函数内的本地变量没什么区别。对全局变量的访问会改写成_ENV.var这样的形式,_ENV为函数的第1个Upval...
-
19
深入Lua:函数和闭包接下来要开始解析函数的原型和闭包对象,这部分内容相对有点多而杂,且理解上会有点难度。如果只是想了解总体的设计思路,建议看一下The Implement...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK