元表
我觉得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) {
// 主体逻辑很简单,根据类型取出ua尿素,然后调用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的flag标记位判断,如果该位存在,表示没有该元方法,直接返回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链太长了。
具体逻辑请看上面这几个函数。
__eq
比较两个对象是否相等,在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。