我们可以使用操作符对 Lua 的值进行运算,例如对数值类型的值进行加减乘除的运算操作以及对字符串的连接、取长操作等(在 Lua 学习笔记(三)—— 表达式 中介绍了许多类似的运算)。元表正是定义这些操作行为的地方。
元表本质上是一个普通 Lua 表。元表中的键用来指定操作,称为“事件名”;元表中键所关联的值称为“元方法”,定义操作的行为。
1 事件名与元方法
仅表(table)类型值对应的元表可由用户自行定义。其他类型的值所对应的元表仅能通过 Debug 库进行修改。
元表中的事件名均以两条下划线 __
作为前缀,元表支持的事件名有如下几个:
__index -- 'table[key]',取下标操作,用于访问表中的域
__newindex -- 'table[key] = value',赋值操作,增改表中的域
__call -- 'func(args)',函数调用,(参见 《Lua 学习笔记(三)—— 表达式》中的函数部分介绍)
-- 数学运算操作符
__add -- '+'
__sub -- '-'
__mul -- '*'
__div -- '/'
__mod -- '%'
__pow -- '^'
__unm -- '-'
-- 连接操作符
__concat -- '..'
-- 取长操作符
__len -- '#'
-- 比较操作符
__eq -- '=='
__lt -- '<' -- a > b 等价于 b < a
__le -- '<=' -- a >= b 等价于 b <= a
还有一些其他的事件,例如 __tostring
和 __gc
等。
下面进行详细介绍。
2 元表与值
每个值都可以拥有一个元表。对 userdata 和 table 类型而言,其每个值都可以拥有独立的元表,也可以几个值共享一个元表。对于其他类型,一个类型的值共享一个元表。例如所有数值类型的值会共享一个元表。除了字符串类型,其他类型的值默认是没有元表的。
使用 getmetatable 函数可以获取任意值的元表。
使用 setmetatable 函数可以设置表类型值的元表。(这两个函数将在[基础函数库]部分进行介绍)
2.1 例子
只有字符串类型的值默认拥有元表:
a = "5"
b = 5
c = {5}
print(getmetatable(a)) --> table: 0x7fe221e06890
print(getmetatable(b)) --> nil
print(getmetatable(c)) --> nil
3 事件的具体介绍
事先提醒 Lua 使用 raw
前缀的函数来操作元方法,避免元方法的循环调用。
例如 Lua 获取对象 obj 中元方法的过程如下:
rawget(getmetatable(obj)or{}, "__"..event_name)
3.1 元方法 index
index 是元表中最常用的事件,用于值的下标访问 — table[key]
。
事件 index 的值可以是函数也可以是表。当使用表进行赋值时,元方法可能引发另一次元方法的调用,具体可见下面伪码介绍。
当用户通过键值来访问表时,如果没有找到键对应的值,则会调用对应元表中的此事件。如果 index 使用表进行赋值,则在该表中查找传入键的对应值;如果 index 使用函数进行赋值,则调用该函数,并传入表和键。
Lua 对取下标操作的处理过程用伪码表示如下:
function gettable_event (table, key)
-- h 代表元表中 index 的值
local h
if type(table) == "table" then
-- 访问成功
local v = rawget(table, key)
if v ~= nil then return v end
-- 访问不成功则尝试调用元表的 index
h = metatable(table).__index
-- 元表不存在返回 nil
if h == nil then return nil end
else
-- 不是对表进行访问则直接尝试元表
h = metatable(table).__index
-- 无法处理导致出错
if h == nil then
error(···);
end
end
-- 根据 index 的值类型处理
if type(h) == "function" then
return h(table, key) -- 调用处理器
else
return h[key] -- 或是重复上述操作
end
end
3.1.1 例子
使用表赋值:
t = {[1] = "cat",[2] = "dog"}
print(t[3]) --> nil
setmetatable(t, {__index = {[3] = "pig", [4] = "cow", [5] = "duck"}})
print(t[3]) --> pig
使用函数赋值:
t = {[1] = "cat",[2] = "dog"}
print(t[3]) --> nil
setmetatable(t, {__index = function (table,key)
key = key % 2 + 1
return table[key]
end})
print(t[3]) --> dog
3.2 元方法 newindex
newindex 用于赋值操作 — talbe[key] = value
。
事件 newindex 的值可以是函数也可以是表。当使用表进行赋值时,元方法可能引发另一次元方法的调用,具体可见下面伪码介绍。
当操作类型不是表或者表中尚不存在传入的键时,会调用 newindex 的元方法。如果 newindex 关联的是一个函数类型以外的值,则再次对该值进行赋值操作。反之,直接调用函数。
~~不是太懂:一旦有了 “newindex” 元方法, Lua 就不再做最初的赋值操作。 (如果有必要,在元方法内部可以调用 rawset 来做赋值。)~~
Lua 进行赋值操作时的伪码如下:
function settable_event (table, key, value)
local h
if type(table) == "table" then
-- 修改表中的 key 对应的 value
local v = rawget(table, key)
if v ~= nil then rawset(table, key, value); return end
--
h = metatable(table).__newindex
-- 不存在元表,则直接添加一个域
if h == nil then rawset(table, key, value); return end
else
h = metatable(table).__newindex
if h == nil then
error(···);
end
end
if type(h) == "function" then
return h(table, key,value) -- 调用处理器
else
h[key] = value -- 或是重复上述操作
end
end
3.2.1 例子
元方法为表类型:
t = {}
mt = {}
setmetatable(t, {__newindex = mt})
t.a = 5
print(t.a) --> nil
print(mt.a) --> 5
通过两次调用 newindex 元方法将新的域添加到了表 mt 。
+++
元方法为函数:
-- 对不同类型的 key 使用不同的赋值方式
t = {}
setmetatable(t, {__newindex = function (table,key,value)
if type(key) == "number" then
rawset(table, key, value*value)
else
rawset(table, key, value)
end
end})
t.name = "product"
t[1] = 5
print(t.name) --> product
print(t[1]) --> 25
3.3 元方法 call
call 事件用于函数调用 — function(args)
。
Lua 进行函数调用操作时的伪代码:
function function_event (func, ...)
if type(func) == "function" then
return func(...) -- 原生的调用
else
-- 如果不是函数类型,则使用 call 元方法进行函数调用
local h = metatable(func).__call
if h then
return h(func, ...)
else
error(···)
end
end
end
3.3.1 例子
由于用户只能为表类型的值绑定自定义元表,因此,我们可以对表进行函数调用,而不能把其他类型的值当函数使用。
-- 把数据记录到表中,并返回数据处理结果
t = {}
setmetatable(t, {__call = function (t,a,b,factor)
t.a = 1;t.b = 2;t.factor = factor
return (a + b)*factor
end})
print(t(1,2,0.1)) --> 0.3
print(t.a) --> 1
print(t.b) --> 2
print(t.factor) --> 0.1
3.4 运算操作符相关元方法
运算操作符相关元方法自然是用来定义运算的。
以 add 为例,Lua 在实现 add 操作时的伪码如下:
function add_event (op1, op2)
-- 参数可转化为数字时,tonumber 返回数字,否则返回 nil
local o1, o2 = tonumber(op1), tonumber(op2)
if o1 and o2 then -- 两个操作数都是数字?
return o1 + o2 -- 这里的 '+' 是原生的 'add'
else -- 至少一个操作数不是数字时
local h = getbinhandler(op1, op2, "__add") -- 该函数的介绍在下面
if h then
-- 以两个操作数来调用处理器
return h(op1, op2)
else -- 没有处理器:缺省行为
error(···)
end
end
end
代码中的 getbinhandler 函数定义了 Lua 怎样选择一个处理器来作二元操作。 在该函数中,首先,Lua 尝试第一个操作数。如果这个操作数所属类型没有定义这个操作的处理器,然后 Lua 会尝试第二个操作数。
function getbinhandler (op1, op2, event)
return metatable(op1)[event] or metatable(op2)[event]
end
+++
对于一元操作符,例如取负,Lua 在实现 unm 操作时的伪码:
function unm_event (op)
local o = tonumber(op)
if o then -- 操作数是数字?
return -o -- 这里的 '-' 是一个原生的 'unm'
else -- 操作数不是数字。
-- 尝试从操作数中得到处理器
local h = metatable(op).__unm
if h then
-- 以操作数为参数调用处理器
return h(op)
else -- 没有处理器:缺省行为
error(···)
end
end
end
3.4.1 例子
加法的例子:
t = {}
setmetatable(t, {__add = function (a,b)
if type(a) == "number" then
return b.num + a
elseif type(b) == "number" then
return a.num + b
else
return a.num + b.num
end
end})
t.num = 5
print(t + 3) --> 8
取负的例子:
t = {}
setmetatable(t, {__unm = function (a)
return -a.num
end})
t.num = 5
print(-t) --> -5
3.5 元方法 tostring
对于 tostring 操作,元方法定义了值的字符串表示方式。
例子:
t = {num = "a table"}
print(t) --> table: 0x7f8e83c0a820
mt = {__tostring = function(t)
return t.num
end}
setmetatable(t, mt)
print(tostring(t)) --> a table
print(t) --> a table
3.6 比较类元方法
对于三种比较类操作,均需要满足两个操作数为同类型,且关联同一个元表时才能使用元方法。
对于 eq (等于)比较操作,如果操作数所属类型没有原生的等于比较,则调用元方法。
对于 lt (小于)与 le (小于等于)两种比较操作,如果两个操作数同为数值或者同为字符串,则直接进行比较,否则使用元方法。
对于 le 操作,如果元方法 “le” 没有提供,Lua 就尝试 “lt”,它假定 a <= b 等价于 not (b < a) 。
3.6.1 例子
等于比较操作:
t = {name="number",1,2,3}
t2 = {name = "number",4,5,6}
mt = {__eq = function (a,b)
return a.name == b.name
end}
setmetatable(t,mt) -- 必须要关联同一个元表才能比较
setmetatable(t2,mt)
print(t==t2) --> true
3.7 其他事件的元方法
对于连接操作,当操作数中存在数值或字符串以外的类型时调用该元方法。
对于取长操作,如果操作数不是字符串类型,也不是表类型,则尝试使用元方法(这导致自定义的取长基本没有,在之后的版本中似乎做了改进)。
3.7.1 例子
取长操作:
t = {1,2,3,"one","two","three"}
setmetatable(t, {__len = function (t)
local cnt = 0
for k,v in pairs(t) do
if type(v) == "number" then
cnt = cnt + 1
print(k,v)
end
end
return cnt
end})
-- 结果是 6 而不是预期中的 3
print(#t) --> 6