前言

上一篇文章 中我们探讨了 Lua 中表的基本结构,以及表的创建、访问及插入元素等操作。但 Lua 中的表还有一个非常重要的特性:元表(metatable)。元表是 Lua 中表的一个重要特性,它可以让我们对表进行一些特殊的操作,比如重载表的操作符、修改表的行为等。我们甚至可以利用元表来实现面向对象编程。本文将继续探讨 Lua 中表的元表。

元表与元方法

每个值都可以有 元表(metatable)(注意,是所有)。元表 是定义了原始数据在某些事件下行为的一个普通Lua表。你可以通过设置其元表的某些特定属性来改变某个值的某些行为。举个例子,一个非数字值进行加法操作时,Lua会在这个值的元表中查找__add属性函数,找到了的情况下,Lua就会调用这个函数来执行加法操作:

1
2
3
4
5
6
7
8
9
10
11
mt = {}
mt.__add = function (a, b)
return a.a + b.a
end

a = {a = 1}
b = {a = 2}
setmetatable(a, mt)
setmetatable(b, mt)

print(a+b) -- 3

元表中的每个事件对应的键都是一个字符串,内容是以两个下划线做前缀的事件名,其相应的值被称为 元值(metavalue)。对于大部分事件,其元值必须是一个称为 元方法(metamethod) 的方法。在上边说的例子里,键值是 _add 字符串且元函数是一个用来做加法操作的方法。若非另有说明,元函数实际上可以是任意可调用的值,它要么是个函数,要么是个带有元方法 __call 的值。

表和full userdata有单独的元表,尽管多个表和userdata之间可以共享它们的元表。其他类型的值共享每个类型的单独元表;即,存在一个单独的元表给所有数字使用,一个单独的元表给所有的字符串使用,等等。默认情况下,值没有元表,但是字符串库给字符串类型设置了一个元表

字符串库所提供的函数都放在名为 string 的表中。其同时会设置字符串值的元表,其中 __index 字段指向了 string 表。因此我们可以使用面向对象的风格来调用字符串函数。例如 string.byte(s,i) 可以写成 s:byte(i) 的形式。例如:

1
2
3
a = "123"
b = a:sub(1,2) -- 12
c = string.sub(a, 1, 2) -- 12

ltm.h 中定义了所有的元方法,这些元方法的标识被放在名为 TMS 的枚举类型中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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_CLOSE,
TM_N /* number of elements in the enum */
} TMS;

tm 即为 tag method 的缩写,这些方法对应的会在 luaT_init 函数中被初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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", "__close"
};
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 */
}
}

上述这些以 __ 开头的字符串就是元方法的名字,它们就是元表中对应元方法的键了。

__index

__index 想必是最常用的元方法了。这个操作发生在 table 不是一个表或者 key 不存在于 table 中的情况下。此时将会在 table 的元表中查找其元值。

此事件的元值可以是一个方法、一个表、或者任何带有 __index 元值的值。如果是方法,它会将 tablekey 作为参数来调用,调用结果(调整为单值)作为操作结果。否则,最终的结果是其元值索引 key 的结果。此索引是常规索引,而非直接索引,所以可以触发其他的 __index 元值(也即触发递归查找)。

利用 __index 元方法,我们可以在 Lua 中模拟面向对象编程的一些行为,如类与继承等,详见 Lua 入门

这里举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
t = {}

function getKey(table, key)
return "function value"
end

setmetatable(t, {__index = getKey})
print("t.key = " .. t.key) -- t.key = function value

mt = {key = "table value"}
setmetatable(t, {__index = mt})
print("t.key = " .. t.key) -- t.key = table value

tt = {}
setmetatable(tt, {__index = t})
print("tt.key = " .. tt.key) -- tt.key = table value

注意如果一个函数被设为 __index 元方法,那么这个函数一定要有两个参数,第一个参数是表本身,第二个参数要查询的键。

__index 的实现

__index 这种逐级查找的行为主要通过 luaV_finishget 函数实现。该函数用于访问一个值的元表,并返回对应元方法的访问结果。注意,这里需要再次强调,任何类型的值都可以有元表,该函数中也会对不同类型的值进行不同的处理。

luaV_finishget 函数的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
** 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 empty).
*/
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 (unlikely(notm(tm)))
luaG_typeerror(L, t, "index"); /* no metamethod */
/* else will try the metamethod */
}
else { /* 't' is a table */
lua_assert(isempty(slot));
tm = fasttm(L, hvalue(t)->metatable, TM_INDEX); /* table's metamethod */
if (tm == NULL) { /* no metamethod? */
setnilvalue(s2v(val)); /* result is nil */
return;
}
/* else will try the metamethod */
}
if (ttisfunction(tm)) { /* is metamethod a function? */
luaT_callTMres(L, tm, t, key, val); /* 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");
}

首先我们可以看到,函数整体是一个循环,这就是逐级查找的过程了,循环的最大次数由 MAXTAGLOOP 定义,Lua 5.4 版本中默认是 20000 次。如果超过了这个值还是没有查询到结果,就会抛出一个错误。

这里以一个递归的例子来说明,最终就会抛出上述的错误:

1
2
3
4
5
6
7
8
9
10
t1 = {}
t2 = {}

t1.__index = t2
t2.__index = t1

setmetatable(t1, t2)
setmetatable(t2, t1)

print(t1.a) -- '__index' chain too long; possible loop

从函数的注释中可以看出,如果 t 是一个表的话,则 slot 会是一个非 NULL 的指针(但肯定是一个空的表项,不然也不会查元表),否则 slot 会是 NULL

slotNULL 时,会调用 luaT_gettmbyobj 函数来获取该值的 __index 元方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, TMS event) {
Table *mt;
switch (ttype(o)) {
case LUA_TTABLE:
mt = hvalue(o)->metatable;
break;
case LUA_TUSERDATA:
mt = uvalue(o)->metatable;
break;
default:
mt = G(L)->mt[ttype(o)];
}
return (mt ? luaH_getshortstr(mt, G(L)->tmname[event]) : &G(L)->nilvalue);
}

可以看到,对于基本类型,会直接从一个全局的数组中取得对应类型的元表,并通过 luaH_getshortstr 的方式访问元表中的元方法。如上述 string 类型的种种库函数,就是通过这种方式实现的。

1
2
3
4
5
6
7
8
9
else {  /* 't' is a table */
lua_assert(isempty(slot));
tm = fasttm(L, hvalue(t)->metatable, TM_INDEX); /* table's metamethod */
if (tm == NULL) { /* no metamethod? */
setnilvalue(s2v(val)); /* result is nil */
return;
}
/* else will try the metamethod */
}

如果查询的对象是一张表的话则会通过 fasttm 宏来获取该表的元表中的 __index 元方法。

fasttm 等相关元方法快速查询的实现也十分巧妙,

首先,Table 结构体中有一个大小为 1 byteflags 字段,用于标记该表是否有某个元方法,这样可以避免重复查询:

1
2
3
4
5
typedef struct Table {
CommonHeader;
lu_byte flags; /* 1<<p means tagmethod(p) is not present */
// ...
} Table;

如果某一位被置为 1,则表示该表中没有对应的元方法。再回到 TMS 的定义中:

1
2
3
4
5
6
7
8
9
typedef enum {
TM_INDEX,
TM_NEWINDEX,
TM_GC,
TM_MODE,
TM_LEN,
TM_EQ, /* last tag method with fast access */
// ...
} TMS;

可以看到,Lua 将最常用的几种元方法放在了前面,TM_EQ 之前的元方法都可以通过 fasttm 宏来快速查询,这也是因为 flags 只有 8 位。

这里不由得感慨 Lua 的设计之精妙,无论是对于空间还是时间的优化都做得非常好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define gfasttm(g,et,e) ((et) == NULL ? NULL : \
((et)->flags & (1u<<(e))) ? NULL : luaT_gettm(et, e, (g)->tmname[e]))

#define fasttm(l,et,e) gfasttm(G(l), et, e)

const TValue *luaT_gettm (Table *events, TMS event, TString *ename) {
const TValue *tm = luaH_getshortstr(events, ename);
lua_assert(event <= TM_EQ);
if (notm(tm)) { /* no tag method? */
events->flags |= cast_byte(1u<<event); /* cache this fact */
return NULL;
}
else return tm;
}

fasttm 宏也正是利用了 flags 字段,如果该表中没有对应的元方法,则会将对应的位设置为 1,并返回 NULL; 否则依然使用 luaH_getshortstr 函数来获取元方法。

1
2
3
4
5
6
7
8
9
10
if (ttisfunction(tm)) {  /* is metamethod a function? */
luaT_callTMres(L, tm, t, key, val); /* 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') */

获取到了元方法后,如果是函数则会调用并将结果存放在 val 中并返回,否则会尝试访问 tm[key];如果上述尝试都没有结果则需要递归查找。

至此,就完成了 __index 元方法的查找过程。

__newindex

__newindex 事件发生在对表中不存在的键赋值时,此时会在表的元表中查找其元值。与 __index 类似,此元值可以是方法、表、或者任何带有 __newindex 元值的值。如果是方法,它会将 tablekeyvalue 作为参数来调用。否则,Lua 将再次对这个元值做索引赋值。这里的赋值流程是常规赋值,而不是直接的赋值,所以它可能会递归触发其他的 __newindex 元值。

无论何时,当 __newindex 元值被调用,Lua不会执行任何更多的赋值操作。如果需要,元函数自身可以调用 rawset 来做赋值。举例来说,如果我们这样写:

1
2
3
4
5
6
7
8
9
10
11
local t = {}
local metatable_t = {
__newindex = function(tt, key, value)
print("call __newindex")
tt[key] = value
end,
}
setmetatable(t, metatable_t)

t[1] = 2
print("t[1]: " .. tostring(t[1]))

看似在 __newindex 中对 tt[key] 进行赋值,十分合理,但实际上这样会导致递归调用,最终会抛出错误,正确的方法是使用 rawset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local t = {}
local metatable_t = {
__newindex = function(tt, key, value)
print("call __newindex")
rawset(tt, key, value)
-- tt[key] = value
end,
}

setmetatable(t, metatable_t)

t[1] = 2

print("t[1]: " .. tostring(t[1]))

-- output:
-- call __newindex
-- t[1]: 2

rawset 函数用于直接将 table[index] 设为 value ,不会使用元值 __newindex。参数 table 必须是一个表,参数 index 可以是除 nilNaN 的任何值,参数 value 可以是任意 Lua 值,此函数会将 table 返回。

可以看到,当 __newindex 元值是一个函数时,传入的第一个参数是表本身,即修改会发生在原表上。

__newindex 的元值也可以是一张表:

1
2
3
4
5
6
7
8
9
local t = {}
local mt = {"value"}

setmetatable(t, {__newindex = mt})

t[1] = "new value"

print(t[1]) -- nil
print(mt[1]) -- new value

相反地,当 __newindex 的元值是一张表时,赋值操作并不会影响到原表,而是会将值设置在元表中,当元表也有元表且实现了 __newindex 时,会递归调用。

__newindex 的实现

__newindex 的实现与 __index 类似,主要位于 luaV_finishset 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
** Finish a table assignment 't[key] = val'.
** If 'slot' is NULL, 't' is not a table. Otherwise, 'slot' points
** to the entry 't[key]', or to a value with an absent key if there
** is no such entry. (The value at 'slot' must be empty, otherwise
** 'luaV_fastget' would have done the job.)
*/
void luaV_finishset (lua_State *L, const TValue *t, TValue *key,
TValue *val, const TValue *slot) {
int loop; /* counter to avoid infinite loops */
for (loop = 0; loop < MAXTAGLOOP; loop++) {
const TValue *tm; /* '__newindex' metamethod */
if (slot != NULL) { /* is 't' a table? */
Table *h = hvalue(t); /* save 't' table */
lua_assert(isempty(slot)); /* slot must be empty */
tm = fasttm(L, h->metatable, TM_NEWINDEX); /* get metamethod */
if (tm == NULL) { /* no metamethod? */
if (isabstkey(slot)) /* no previous entry? */
slot = luaH_newkey(L, h, key); /* create one */
/* no metamethod and (now) there is an entry with given key */
setobj2t(L, cast(TValue *, slot), val); /* set its new value */
invalidateTMcache(h);
luaC_barrierback(L, obj2gco(h), val);
return;
}
/* else will try the metamethod */
}
else { /* not a table; check metamethod */
tm = luaT_gettmbyobj(L, t, TM_NEWINDEX);
if (unlikely(notm(tm)))
luaG_typeerror(L, t, "index");
}
/* try the metamethod */
if (ttisfunction(tm)) {
luaT_callTM(L, tm, t, key, val);
return;
}
t = tm; /* else repeat assignment over 'tm' */
if (luaV_fastget(L, t, key, slot, luaH_get)) {
luaV_finishfastset(L, t, slot, val);
return; /* done */
}
/* else 'return luaV_finishset(L, t, key, val, slot)' (loop) */
}
luaG_runerror(L, "'__newindex' chain too long; possible loop");
}

函数的主体部分也是一个循环,用于逐级查找;如果 t 是一张表且元表中没有 __newindex 元方法,则会直接将值赋给 t[key],如果没有这个键则会调用 luaH_newkey 创建一个键。否则会分类别调用元方法。

如果元方法是一个函数,则会调用并返回,否则会尝试访问 tm[key],如果有结果则会调用 luaV_finishfastset 函数完成赋值操作(也即不会触发进一步的递归查找),否则会递归查找。

__gc

__gc 元函数被称为终结器(finalizers),于垃圾收集器发现相应的表或 userdata 将要被销毁时被调用。finalizers 允许你将垃圾收集器与外部资源管理协调起来,例如关闭文件、网络或数据库连接,或者释放你自己的内存

__gc 的作用与 C# 中的 Finalize 方法有异曲同工之妙,都是用于非托管资源的释放。下面可以看到,它们也有着相同的缺点。

对于收集时要终结的对象(表或者 userdata ),需要把它标记为可触发终结(finalization)。如果想将标记一个对象,我们需要设置一个对象的元表并且此元表要有元函数__gc注意,当你设置元表时没有 __gc 属性,而是之后再于元表上创建这个属性的话,这个对象将不会被标记

当一个被标记的对象死亡时,其并不会立刻被垃圾收集器收集起来。相反,Lua会将其放到一个列表中。Lua 在收集完成后遍历这个列表。对于列表中的每个对象都会查找其是否有元函数__gc,如果有,Lua 将这个对象作为单一参数来调用此函数。

在垃圾收集周期的最后,终结器会以和标记顺序相反的顺序来调用该周期内收集的对象的终结器。终结器的调用可能发生在常规代码执行时的任意时刻

因为被回收的对象仍然会被终结器使用,所以其一定会被Lua复原(包括被其唯一关联的其他对象)。通常,这个复原是暂时的,而且其内存会在下一次GC周期内被释放。然而,如果终结器将对象保存到了某些全局位置(例如全局变量),那么这个复原就是持续的。此外,如果终结器将一个正在被终结的对象再次标记为可触发终结,那么终结器会在下个GC周期时的对象死亡的地方被再次调用。在任意情况下,没有被标记为可触发终结并已经死亡的对象,其内存只可能在GC周期内被释放。

下面举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
local t = {}
local mt = {
__gc = function (o)
print("release object: ", o)
end
}

setmetatable(t, mt)
print("done")

-- output:
-- done
-- release object: table: 0000000000fa99d0

可以看到,在执行完一个 Lua 文件后,进行了一次垃圾回收,此时 __gc 函数被调用。当然我们也可以手动触发垃圾回收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local t = {}
local mt = {
__gc = function (o)
print("release object: ", o)
end
}

setmetatable(t, mt)
t = nil
collectgarbage()

print("done")

-- output:
-- release object: table: 0000000000ed9f90
-- done

__gc 的实现

__gc 的实现主要位于 lgc.c 中的 GCTM 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void GCTM (lua_State *L) {
global_State *g = G(L);
const TValue *tm;
TValue v;
lua_assert(!g->gcemergency);
setgcovalue(L, &v, udata2finalize(g));
tm = luaT_gettmbyobj(L, &v, TM_GC);
if (!notm(tm)) { /* is there a finalizer? */
int status;
lu_byte oldah = L->allowhook;
int running = g->gcrunning;
L->allowhook = 0; /* stop debug hooks during GC metamethod */
g->gcrunning = 0; /* avoid GC steps */
setobj2s(L, L->top++, tm); /* push finalizer... */
setobj2s(L, L->top++, &v); /* ... and its argument */
L->ci->callstatus |= CIST_FIN; /* will run a finalizer */
status = luaD_pcall(L, dothecall, NULL, savestack(L, L->top - 2), 0);
L->ci->callstatus &= ~CIST_FIN; /* not running a finalizer anymore */
L->allowhook = oldah; /* restore hooks */
g->gcrunning = running; /* restore state */
if (unlikely(status != LUA_OK)) { /* error while running __gc? */
luaE_warnerror(L, "__gc metamethod");
L->top--; /* pops error object */
}
}
}

该函数会在一个表或者 userdata 将要被销毁时被调用。首先会获取到该对象的元表中的 __gc 元方法,如果存在则会调用该方法。

__mode

__mode 元方法用于控制元表的数据引用方式。 __mode 的值是一个字符串,共有以下三种取值:

  • k:表示弱引用键
  • v:表示弱引用值
  • kv:表示弱引用键和值

这里弱引用的含义与 C++ 中的 weak_ptr 类似,即弱引用不会增加对象的引用计数,如果仅有弱引用指向一个对象,那么这个对象会被垃圾回收器回收。举例来说:

1
2
3
4
5
6
7
local key = {"key"}
local value = {"value"}
local t = {[key] = value}

value = nil
collectgarbage()
print(t.key[1]) -- value

在上述代码中,value 被赋值为 nil,但是 t.key 仍然可以访问到 value,这是因为 t 强引用value。如果我们将 t 的元表设置为弱引用值:

1
2
3
4
5
6
7
8
local key = {"key"}
local value = {"value"}
local t = {[key] = value}
setmetatable(t, {__mode = "v"})

value = nil
collectgarbage()
print(t.key) -- nil

此时 t.keynil,因为 t 弱引用value,所以在 value 被赋值为 nil 并进行垃圾回收后,value 本身被回收。

1
2
3
4
5
6
7
8
9
10
11
local key = {"key"}
local value = {"value"}
local t = {[key] = value}
setmetatable(t, {__mode = "k"})

key = nil
collectgarbage("collect")

for k, v in pairs(t) do
print(k, v)
end

同样地,我们也可以设置弱引用键,此时 t 弱引用key,所以在 key 被赋值为 nil 并进行垃圾回收后,t 中的这一节点也将被回收。

__mode 元方法可以延伸出一种特殊的表:弱表(Weak Tables):

弱表(Weak Tables) 就是一个其元素为弱引用的表。弱引用会被GC忽略。换言之,一个仅被弱引用指向的对象会被GC回收。

一个弱表可以同时或任一拥有弱键和弱值。拥有弱值的表允许收集其值,但是会阻止收集其键。一个同时拥有弱值和弱键的表的键和值都允许收集。任意情况下,当键或值被收集,其键值对都会从表中移除。表的“弱性”由元表中的__mode属性来控制。此元值如果存在,则必须为给出的字符串之一:“k”——表示拥有弱键的表,"v"表示拥有弱值的表,"kv"表示同时拥有弱键和弱值的表。

拥有弱键和强值的表也被称为临时表(ephemeron table)。在临时表中,键可达是值可达的前提。具体来说,某个键只被其值引用时,这个键值对将被删除。

任何对表的“弱性”做的修改都会在下一次GC周期生效。比如,当你将“弱性”改成一个更强的模式时,Lua仍然会在改动生效之前回收一些东西。

只有显式构建的对象才会从弱表中移除。纯值,例如数字和轻量C函数,其不受 GC 的约束,因此它们不会从弱表中移除(除了收集它们所关联的值)。尽管字符串受制于GC,但其没有显式的结构且与纯值平等,比起对象其行为更类似于值。因此其也不会从弱表中移除

复原的对象(即,正被执行终结的对象,或仅被正在终结的对象所引用的对象)在弱表中的行为比较特殊。当对象明确被释放时,于弱值中的将会在执行终结之前被移除,而在弱键中的则只会在执行终结后的下一次收集中被移除。这是为了允许终结器通过弱表来访问与其对象所关联的属性。

如果弱表在一个收集周期中被复原的值里,那么在下个循环之前可能都没有得到适当的清理。

__mode 的实现

__mode 的实现主要位于 traversetable 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static lu_mem traversetable (global_State *g, Table *h) {
const char *weakkey, *weakvalue;
const TValue *mode = gfasttm(g, h->metatable, TM_MODE);
markobjectN(g, h->metatable);
if (mode && ttisstring(mode) && /* is there a weak mode? */
(cast_void(weakkey = strchr(svalue(mode), 'k')),
cast_void(weakvalue = strchr(svalue(mode), 'v')),
(weakkey || weakvalue))) { /* is really weak? */
black2gray(h); /* keep table gray */
if (!weakkey) /* strong keys? */
traverseweakvalue(g, h);
else if (!weakvalue) /* strong values? */
traverseephemeron(g, h, 0);
else /* all weak */
linkgclist(h, g->allweak); /* nothing to traverse now */
}
else /* not weak */
traversestrongtable(g, h);
return 1 + h->alimit + 2 * allocsizenode(h);
}

该函数主要在垃圾回收的过程中被调用,用于遍历表并对其中引用到的所有元素都进行标记。在遍历表的过程中,会检查表的元表中是否有 __mode 元方法,如果有则会根据其值来设置表的弱引用方式。这部分的具体逻辑涉及到 Lua 的 gc 过程,以及三色标记法等,我们会在 gc 部分再做介绍。

__len

__len 元方法用于重载 # 操作符,即获取表或者字符串的长度。如果对象不是一个字符串,Lua 将尝试调用其元函数。如果元函数存在,则调用将对象作为参数调用元函数,并将调用结果(通常调整为单个值)作为操作结果。如果元表不存在但是对象是 table,Lua 使用表的取长操作,也即 luaH_getn,详见表(上)。否则,Lua抛出将会抛出错误。

举例来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local t = {1, 2, 3, 4, 5, 6, 7, 8}
print(#t) -- 8

setmetatable(t, {
__len = function(t)
local count = 0
for k, v in pairs(t) do
if k % 2 == 0 then
count = count + 1
end
end
return count
end
})

print(#t) -- 4

在上述代码中,首先会调用默认的 luaH_getn 函数获取表的长度;当我们设置了 __len 元方法后,会调用该方法来获取表的长度,这里即会返回表中偶数索引的个数。

__len 的实现

__len 的实现主要位于 luaV_objlen 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
** Main operation 'ra = #rb'.
*/
void luaV_objlen (lua_State *L, StkId ra, const TValue *rb) {
const TValue *tm;
switch (ttypetag(rb)) {
case LUA_VTABLE: {
Table *h = hvalue(rb);
tm = fasttm(L, h->metatable, TM_LEN);
if (tm) break; /* metamethod? break switch to call it */
setivalue(s2v(ra), luaH_getn(h)); /* else primitive len */
return;
}
case LUA_VSHRSTR: {
setivalue(s2v(ra), tsvalue(rb)->shrlen);
return;
}
case LUA_VLNGSTR: {
setivalue(s2v(ra), tsvalue(rb)->u.lnglen);
return;
}
default: { /* try metamethod */
tm = luaT_gettmbyobj(L, rb, TM_LEN);
if (unlikely(notm(tm))) /* no metamethod? */
luaG_typeerror(L, rb, "get length of");
break;
}
}
luaT_callTMres(L, tm, rb, rb, ra);
}

该函数用于获取表或者字符串等对象的长度。可以看到这里针对表会先判断是否有 __len 元方法,如果有则会调用该方法,否则会调用 luaH_getn 函数获取表的长度。

其他元方法

除上述的这些常用元方法外,Lua 还有其他一些元方法,它们都用于重载不同的运算符,如 +-*/ 等,增加了这些元方法作用类似于 C++ 中的运算符重载,可以使得对于表的操作更加灵活。重载运算符的元方法主要可以分为三类:

  1. 算术操作运算符:

    • TM_ADD:加法操作,符号 +
    • TM_SUB:减法操作,符号 -
    • TM_MUL:乘法操作,符号 *
    • TM_MOD:取模操作,符号 %
    • TM_POW:幂运算,符号 ^
    • TM_DIV:除法操作,符号 /
    • TM_IDIV:整除操作,符号 //
  2. 位操作运算符:

    • TM_BAND:按位与操作,符号 &
    • TM_BOR:按位或操作,符号 |
    • TM_BXOR:按位异或操作,符号 ~
    • TM_SHL:左移操作,符号 <<
    • TM_SHR:右移操作,符号 >>
    • TM_UNM:取负操作,符号 -
    • TM_BNOT:按位取反操作,符号 ~
  3. 比较操作运算符:

    • TM_EQ:等于操作,符号 ==
    • TM_LT:小于操作,符号 <
    • TM_LE:小于等于操作,符号 <=
  4. 其他操作:

    • TM_CONCAT:连接操作,符号 ..
    • TM_CALL:调用操作,符号 ()
    • TM_CLOSE:关闭操作,常与 goto 一起使用,这里不做详细介绍

通过定义这些元方法,我们可以对表的操作进行重载,使得表的操作更加灵活。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local t1 = {1, 2, 3}
local t2 = {4, 5, 6}

setmetatable(t1, {
__add = function(t1, t2)
local t = {}
for i = 1, #t1 do
t[i] = t1[i] + t2[i]
end
return t
end
})

local t3 = t1 + t2

for i = 1, #t3 do
print(t3[i])
end

-- output:
-- 5
-- 7
-- 9