触类旁通:从元表到元方法,Lua编程的奇妙之旅

一、元表的概念

元表可以修改一个值在面对一个未知操作时的行为。可以认为,元表是面向对象领域中的受限制 像类一样,元表定义的是实例的行为。
由于元表只能给出预先定义的操作集合的行为,所以元表比类更受限;同时,元表也不支持继承。

Lua 语言中的每一个值都可以有元表,每一个表和用户数据类型都具有各自独立的元表,而其他类型的值则共享对应类型所属的同一个元素。

Lu 语言在创建新表时不带元表:

Lua 5.3.6  Copyright (C) 1994-2020 Lua.org, PUC-Rio
> t={}
> print(getmetatable(t))
nil

可以使用函数 setmetatable 来设置或修改任意表的元表:

> t1={}
> setmetatable(t,t1)
table: 0x1ca1220
> print(getmetatable(t)==t1)
true

Lu 语言中只能为表设置元表; 如果要为其它类型值设置元表,则必须通过C代码或调试库完成。字符
串标准库为所有的字符串都设置了同一个元表,而其他类型在默认情况都没有元表。

> print(getmetatable("hello"))
table: 0x1c81a70
> print(getmetatable("world"))
table: 0x1c81a70
> print(getmetatable(100))
nil
> print(getmetatable(print))
nil

二、算术运算相关的元方法

每种算术运算符都有一个对应的元方法,有加法(__add )、乘法(__mul)、减法( __sub )、除法( __div)、floor除法( __idiv )、负数( __unm )、取模( __mod )和幕运算( __pow )。

类似的,位操作 有元方法 按位与 __band )、按位或( __bor )、按位异或(__bxor)、按位取反(__bnot)、左移位( __shl) 、向右移位( __shr)。

可以使用字段__concat 来定义连接运算符的行为。

加法(__add )使用示例,显示两个表的并集。

local mt={}
local Set={}
function Set.new (L)
        local set={}
        setmetatable(set,mt)
        for _, v in ipairs(L) do set[v]=true end
        return set
end

function Set.union(a,b)
        local res=Set.new{}
        for k in pairs(a) do res[k]=true end
        for k in pairs(b) do res[k]=true end
        return res
end

function Set.tostring(set)
        local l={}
        for e in pairs(set) do
                l[#l+1]=tostring(e)
        end
        return "{" .. table.concat(l,",") .. "}"
end

mt.__add=Set.union

s1=Set.new{1,3,5,7,9}
s2=Set.new{2,4,7}
print(getmetatable(s1))
print(getmetatable(s2))

s3=s1+s2
print(Set.tostring(s3))

显示:

table: 0xcd5aa0
table: 0xcd5aa0
{1,2,3,4,5,7,9}

三、关系运算相关的元方法

等于( __eq )、小于( __lt ) 和小于等于( __le)。其他 个关系运算符没有单独的元方法, Lua 语言会将 转换为a~=b转换为not (a == b); a >b 转换为 b < a , a >=b 转换为 b<= a。

示例:

local mt={}
local Set={}
function Set.new (L)
        local set={}
        setmetatable(set,mt)
        for _, v in ipairs(L) do set[v]=true end
        return set
end

function Set.union(a,b)
        local res=Set.new{}
        for k in pairs(a) do res[k]=true end
        for k in pairs(b) do res[k]=true end
        return res
end

mt.__add=Set.union

mt.__le=function(a,b)--子集
        for k in pairs(a) do
                if not b[k] then return false end
        end
        return true
end

mt.__lt=function(a,b)--真子集
        return a<=b and not (b<=a)
end

mt.__eq=function(a,b)
        return a<=b and b<=a
end

s1=Set.new{2,4}
s2=Set.new{4,10,2}

print(s1<=s2)
print(s1<s2)
print(s1>=s1)
print(s1>s1)

true
true
true
false

四、库定义相关的元方法

lua语言虚拟机会检测一个操作中涉及的值是否有存在对应元方法的元表。不过,由于元表是一个普通的表,所以任何人都可以使用它们。因此,程序库在元表中定义和使用它们自己的字段是一种常见的方式。

函数 tostring 是一个典型的例子,函数 tostring 能将表表示为一种简单的文本格式;函数 print 总是调用 tostring来进行格式化输出。不过,当对值进行格式化时,函数 tostring 会首先检查值是否有一个元方法__tostring;如果有,函数 tostring 就调用这个元方法来完成工作,将对象作为参数传给该函数,然后把元方法的返回值作为函数 tostring返回值。

示例:

local mt={}
local Set={}
function Set.new (L)
        local set={}
        setmetatable(set,mt)
        for _, v in ipairs(L) do set[v]=true end
        return set
end

function Set.union(a,b)
        local res=Set.new{}
        for k in pairs(a) do res[k]=true end
        for k in pairs(b) do res[k]=true end
        return res
end

function Set.tostring(set)
        local l={}
        for e in pairs(set) do
                l[#l+1]=tostring(e)
        end
        return "{" .. table.concat(l,",") .. "}"
end

mt.__add=Set.union

mt.__tostring=Set.tostring
s4=Set.new{1,4,7,9}
print(s4)
{1,4,9,7}

函数 setmetatable和getmetatable 也用到了元方法,用于保护元表。
函数 pairs 有了对应的元方法,因此可以修改表被遍历的方式和为非表的对象增加遍历行为。当一个对象拥有__pairs元方法时, pairs会调用这个元方法来完成遍历。

五、表相关的元方法

Lu 语言还提供了一种改变表在两种正常情况下的行为的方式,即访问和修改表中不存在的字段。

5.1、__index元方法

当访 一个表中不存在的字段时会得到 nil 这是正确的,但不是完整的真相。实际上,这些访 问会引发解释器查 一个名为__index 的元方法;如果没有这个元方法,那么像一般情况下一样,结果就是 nil,如果有,就由这个元方法来提供最终结果。

示例:

local proto={x=0,y=0,w=100,h=100}
local mt={}
function new(o)
        setmetatable(o,mt)
        return o
end

mt.__index=function(_,key)
        return proto[key]
end

my=new{x=10,y=20}
print(my.w)
print(my.h)

没有设置w和h,会调用__index元方法,显示:

100
100

lua 语言中,使用元方法__index 来实现继承是很普遍的方法,虽然被叫作方法,但元方法 __index 不一定必须是一个函数,它还可以是一个表。
将一个表用作__index 元方法为实现单继承提供了一种简单快捷的方法。虽然将函数用作元方法开销更昂贵,但函数却更加灵活,可以通过函数来实现多继承、缓存及其他一些变体。
如果希望在访问 个表时不调用__index 元方法,那么可以使用函数 rawget。

5.2、__newindex元方法

元方法 __newindex __index 类似,不同之处在于前者用于表的更新而后者用于表的查询。当对一个表中不存在的索引赋值时,解释器就会查找 __newindex 元方法,如果这个元方法存在,那么解释器就调用它而不执行赋值。

示例:

local proto={x=0,y=0,w=100,h=100}
local mt2={}
function new(o)
        setmetatable(o,mt2)
        return o
end

mt2.__index=function(_,key)
        return proto[key]
end

mt2.__newindex=function(_,key)
         print(tostring(key) .. " do not exist")
        return nil
end

my=new{x=10,y=10}
my[1]=100
print(my.y)

1 do not exist
10

像元方法__index 一样,如果这个元方法是个表,解释器就在此表中执行赋值,而不是在原始的表中进行赋值。此外,还有一个原始函数允许绕过元方法:调用rawset(t, k,v)来等价于 t[k]=v,但不涉及任何元方法。

组合使用元方法__index和__newindex可以实现Lua语言中一些强大的结构。比如只读的表、具有默认值的表和面向对象编程中的继承。

具有默认值的表:

function setDefault(t,d)
        local mt={__index=function() return d end}
        setmetatable(t,mt)
end

tab={x=10,y=20}
print(tab.x,tab.h)
setDefault(tab,0)
print(tab.x,tab.h)

10	nil
10	0

六、元表的实现原理

lua在初始化时,首先会调用luaT_init 函数初始化其中定义的几种元方法对应的字符串。这些都是全局共用的,在初始化完毕之后只可读不可写,也不能回收。
(lstate.h)

/*
** 'global state', shared by all threads of this state
*/
typedef struct global_State {
  lua_Alloc frealloc;  /* function to reallocate memory */
  void *ud;         /* auxiliary data to 'frealloc' */
  l_mem totalbytes;  /* number of bytes currently allocated - GCdebt */
  l_mem GCdebt;  /* bytes allocated not yet compensated by the collector */
  lu_mem GCmemtrav;  /* memory traversed by the GC */
  lu_mem GCestimate;  /* an estimate of the non-garbage memory in use */
  stringtable strt;  /* hash table for strings */
  TValue l_registry;
  unsigned int seed;  /* randomized seed for hashes */
  lu_byte currentwhite;
  lu_byte gcstate;  /* state of garbage collector */
  lu_byte gckind;  /* kind of GC running */
  lu_byte gcrunning;  /* true if GC is running */
  GCObject *allgc;  /* list of all collectable objects */
  GCObject **sweepgc;  /* current position of sweep in list */
  GCObject *finobj;  /* list of collectable objects with finalizers */
  GCObject *gray;  /* list of gray objects */
  GCObject *grayagain;  /* list of objects to be traversed atomically */
  GCObject *weak;  /* list of tables with weak values */
  GCObject *ephemeron;  /* list of ephemeron tables (weak keys) */
  GCObject *allweak;  /* list of all-weak tables */
  GCObject *tobefnz;  /* list of userdata to be GC */
  GCObject *fixedgc;  /* list of objects not to be collected */
  struct lua_State *twups;  /* list of threads with open upvalues */
  unsigned int gcfinnum;  /* number of finalizers to call in each GC step */
  int gcpause;  /* size of pause between successive GCs */
  int gcstepmul;  /* GC 'granularity' */
  lua_CFunction panic;  /* to be called in unprotected errors */
  struct lua_State *mainthread;
  const lua_Number *version;  /* pointer to version number */
  TString *memerrmsg;  /* memory-error message */
  TString *tmname[TM_N];  /* array with tag-method names */
  struct Table *mt[LUA_NUMTAGS];  /* metatables for basic types */
  TString *strcache[STRCACHE_N][STRCACHE_M];  /* cache for strings in API */
} global_State;

(ltm.h)

/*
* WARNING: if you change the order of this enumeration,
* grep "ORDER TM" and "ORDER OP"
*/
typedef enum {
  TM_INDEX,
  TM_NEWINDEX,
  TM_GC,
  TM_MODE,
  TM_LEN,
  TM_EQ,  /* last tag method with fast access */
  TM_ADD,
  TM_SUB,
  TM_MUL,
  TM_MOD,
  TM_POW,
  TM_DIV,
  TM_IDIV,
  TM_BAND,
  TM_BOR,
  TM_BXOR,
  TM_SHL,
  TM_SHR,
  TM_UNM,
  TM_BNOT,
  TM_LT,
  TM_LE,
  TM_CONCAT,
  TM_CALL,
  TM_N          /* number of elements in the enum */
} TMS;

(ltm.c)

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 */
  }
}

遍历前面定义的枚举类型TMS ,将每一个类型对应的字符串赋值给global_State结构体中的tmname ,同时调用函数luaC_fix将这些字符串设置为不可回收的,因为在这个系统运行的过程中,这些字符串会一直用到。

lua虚拟机从一个表中查询数据的过程,其中luaV_gettable 函数的代码如下:
(lvm.h)

/*
** fast track for 'gettable': if 't' is a table and 't[k]' is not nil,
** return 1 with 'slot' pointing to 't[k]' (final result).  Otherwise,
** return 0 (meaning it will have to check metamethod) with 'slot'
** pointing to a nil 't[k]' (if 't' is a table) or NULL (otherwise).
** 'f' is the raw get function to use.
*/
#define luaV_fastget(L,t,k,slot,f) \
  (!ttistable(t)  \
   ? (slot = NULL, 0)  /* not a table; 'slot' is NULL and result is 0 */  \
   : (slot = f(hvalue(t), k),  /* else, do raw access */  \
      !ttisnil(slot)))  /* result not nil? */

/*
** standard implementation for 'gettable'
*/
#define luaV_gettable(L,t,k,v) { const TValue *slot; \
  if (luaV_fastget(L,t,k,slot,luaH_get)) { setobj2s(L, v, slot); } \
  else luaV_finishget(L,t,k,v,slot); }

(lvm.c)

/*
** Finish the table access 'val = t[key]'.
** if 'slot' is NULL, 't' is not a table; otherwise, 'slot' points to
** t[k] entry (which must be nil).
*/
void luaV_finishget (lua_State *L, const TValue *t, TValue *key, StkId val,
                      const TValue *slot) {
  int loop;  /* counter to avoid infinite loops */
  const TValue *tm;  /* metamethod */
  for (loop = 0; loop < MAXTAGLOOP; loop++) {
    if (slot == NULL) {  /* 't' is not a table? */
      lua_assert(!ttistable(t));
      tm = luaT_gettmbyobj(L, t, TM_INDEX);
      if (ttisnil(tm))
        luaG_typeerror(L, t, "index");  /* no metamethod */
      /* else will try the metamethod */
    }
    else {  /* 't' is a table */
      lua_assert(ttisnil(slot));
      tm = fasttm(L, hvalue(t)->metatable, TM_INDEX);  /* table's metamethod */
      if (tm == NULL) {  /* no metamethod? */
        setnilvalue(val);  /* result is nil */
        return;
      }
      /* else will try the metamethod */
    }
    if (ttisfunction(tm)) {  /* is metamethod a function? */
      luaT_callTM(L, tm, t, key, val, 1);  /* call it */
      return;
    }
    t = tm;  /* else try to access 'tm[key]' */
    if (luaV_fastget(L,t,key,slot,luaH_get)) {  /* fast track? */
      setobj2s(L, val, slot);  /* done */
      return;
    }
    /* else repeat (tail call 'luaV_finishget') */
  }
  luaG_runerror(L, "'__index' chain too long; possible loop");
}

是表,则尝试根据key在该表中查找数据,如果找到了非空数据,或者找到该表的元方法表中 index 为空,都返回查找结果。反之,如果不返回查找结果,只可能是上面两个条件的反面情况,即在原表中查找的数据为空,同时原表的元方法表存在_index成员,而且此时该成员已经赋值给了tm。

七、总结

  1. 一个表可以成为任意值的元表,一组相关的表也可以共享一个描述了它们共同行为的通用元表,一个表还可以成为它自己的元表,用于描述其自身特有的行为。总之,任何配置都是合法的。

  2. 只有 table 和 userdata 对象有独自的元表,其他类型只有类型元表。

  3. 只有 table 可以在 lua 中修改设置元表。

  4. userdata 只能在 c 中修改设置元表,lua 中不能修改userdata 元表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值