深入Lua:元表

本文围绕Lua的元表和元方法展开。介绍了Table和userdata对象可单独设置元表,字符串库为string类型设元表。阐述元表通过元方法影响对象行为,还提及快速访问元方法的方式,最后列举了__index、__eq等多种元方法的调用流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

元表

我觉得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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值