通常,Lua中的每个值都有一套预定义的操作集合。例如,可以将数字相加,可以连接字符串。但是我们无法将两个table相加,无法对函数作比较,也无法调用一个字符串。因此可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。例如,假设a和b都是table,通过元表可以定义如何计算表达式a+b。当Lua试图将两个table相加时,它会先检查两者之一是否有元表,然后检查该元表中是否有一个叫__add的字段。如果Lua找到了该字段,就调用该字段对应的值。这个值也就是“元方法”,它应该是一个函数。
简单地说,元表和元方法就类似于C++、Java等语言中的操作符重载。
默认情况下,除了table和userdata类型,其他的类型默认共享一个元表,因此可以实现诸如以下的这些操作
local a = 1
local b = 2
c = a+b
或
local d ="s1"
local e ="s2"
local f = d..e
在lua代码中声明和定义一个变量的时候,不会为这个变量创建一个新的元表,因此如果想实现两个table相加或相减等操作,得为这两个table设置一个元表,同时赋予相加操作的元方法。例如,重载"+"操作符,使其能允许两个table相加,则需重写元表中的__add方法,重写了__add元方法后,当使用”+”操作符时,如果操作数经过tonumber为nil,如果不为nil,则调用原生的”+”号方法,如果为nil,则检查两个操作数中是否含有__add方法,如果有,则执行__add方法,否则抛出异常。除了算术类元方法__add,lua还提供了其余操作符号的元方法关键字,包括关系类元方法和库定义的元方法。
__add | 符号"+"的元方法(二元操作符,相加) |
__sub | 符号"-"的元方法 (二元操作符,相减) |
__mul | 符号"*"的元方法 (二元操作符,相乘) |
__div | 符号"/"的元方法 (二元操作符,相除) |
__mod | 符号"%"的元方法 (二元操作符,取余) |
__pow | 符号"^"的元方法 (二元操作符,次幂) |
__unm | 符号"-"的元方法(一元操作符,正负取反) |
__concat | 符号".."的元方法 (二元操作符,连接) |
__len | 符号"#"的元方法(一元操作符,取长度) |
__eq | 符号"=="的元方法 (二元操作符,是否相等) |
__lt | 符号"<"或">"的元方法 (二元操作符,小于) |
__le | 符号"<="或">="的元方法 (二元操作符,小于等于) |
__index | table元素的下标 |
__newindex | table新分配元素的下标 |
__call | 调用一个变量 |
__tostring | 格式化为字符串 |
__metatable | 元表 |
__mode | 引用模式(用于设置一个值是否是弱引用) |
(1)__add元方法。
__add元方法对应符号”+”。对于相加的两个操作数op1与op2,op1+op2会进行如下操作:
function add_event (op1, op2)
local o1,o2 = tonumber(op1), tonumber(op2)
if o1 ando2 then -- (是否op1和op2都可转为数字?)
returno1 + o2 -- (这里的"+"代表原始的加法)
else -- (如果有一个及以上的操作数不能转化为数字)
--(获取操作数的__add元方法)
localh = getbinhandler(op1, op2, "__add")
ifh then
--(调用这个__add元方法,并将两个操作数传递给这个元方法)
return(h(op1, op2))
else -- (如果没有元方法,抛出异常)
error(···)
end
end
end
以下示例重载__add方法实现两个table相加。
输出
(2)__sub元方法。
__sub元方法对应符号”-”。
以下示例重载__sub方法实现两个table相减。
输出
__mul、__div、__mod等算术类元方法的实现与(1)和(2)相似。
(3)__unm元方法。
__unm元方法是lua中正负数取反的元方法,对应一元操作符”-”,调用__unm元方法的逻辑原型如下:
function unm_event (op)
local o =tonumber(op)
if othen -- (操作符是否是数字?)
return-o -- (取反返回,这里的"-"是原始的取反方法)
else -- (如果操作数不是数字)
-- (获取__unm元方法)
localh = metatable(op).__unm
ifh then
--(调用__unm元方法)
return(h(op))
else -- (抛出异常)
error(···)
end
end
end
以下示例重载__unm方法实现对一个table取反。
输出
(4)__eq元方法。
__eq是lua中关系型的元方法,用于判断两个操作数是否相等,即”==”。lua提供的元方法字段中并没有"~="符号的元方法,因为lua代码被编译时将"~="转化成了not(a==b);同样的,a>b会转化为b<a,a>=b转化为b<=a。
关系型的元方法也会对传入元方法的多个元素进行自己的操作。
__eq示例判断两个table是否相等。
输出
(5)__tostring元方法。
以上的元方法都是基于操作符号,lua中还有基于关键字的元方法,这种元方法叫做库定义元方法。比如我们使用print(value)关键字的时候,总能将value的类型格式化成符合print关键字语法的类型,print关键字在使用时会在value的元表中查找__tostring元方法,类似于__tostring就是库定义元方法。
我们来对table进行库定义方法的修改
__tostring示例打印一个table。
输出
(6)__concat元方法。
__concat元方法是lua中字符串连接的方法,对应二元操作符”..”,调用__concat元方法的逻辑原型如下:
function concat_event (op1, op2)
if(type(op1) == "string" or type(op1) == "number") and
(type(op2)== "string" or type(op2) == "number") then
returnop1 .. op2 -- (这里的".."是原始的字符串连接操作)
else
localh = getbinhandler(op1, op2, "__concat")
ifh then
return(h(op1, op2))
else
error(···)
end
end
end
以下示例重载__concat元方法实现连接两个table的字符串。
输出
(7)__len元方法。
__len元方法是lua中取操作符长度的方法,对应一元操作符”#”,调用__len元方法的逻辑原型如下:
function len_event (op)
if type(op) == "string" then
return strlen(op) -- (原始的取字符串长度)
elseif type(op) == "table" then
return #op -- (原始的取table长度)
else
local h = metatable(op).__len
if h then
return (h(op))
else
error(···)
end
end
end
(8)__lt元方法。
__lt元方法是lua中“小于”的方法,对应一元操作符”<”,调用__lt元方法的逻辑原型如下:
function lt_event (op1, op2)
if type(op1) == "number" and type(op2) =="number" then
return op1 < op2 -- (对数字进行原始的"<"操作)
elseif type(op1) == "string" and type(op2) =="string" then
return op1 < op2 -- (对字符串进行原始的"<操作")
else
local h = getcomphandler(op1, op2, "__lt")
if h then
return (h(op1, op2))
else
error(···)
end
end
end
以下示例重载__lt元方法实现两个数字table的大小比较。
输出
(9)__le元方法。
__le元方法是lua中“小于等于”的方法,对应一元操作符”<=”,其函数原型与重写方法与__lt类似。
(10)__index元方法。
__index元方法用于索引一个table的元素。对于table[key],如果table[key]为nil,则会调用__index元方法,如果__index返回的值为nin,table[key]才会返回nil。
调用__index元方法的逻辑原型如下:
function gettable_event (table, key)
local h
iftype(table) == "table" then
localv = rawget(table, key) -- (用原始的方法获得table[key]值)
ifv ~= nil then return v end
h =metatable(table).__index --(如果table[key]为空,调用__index)
ifh == nil then return nil end
else
h =metatable(table).__index
ifh == nil then
error(···)
end
end
if type(h)== "function" then
return(h(table, key)) -- (如果__index是function类型,调用它)
elsereturn h[key] -- (如果__index是table类型,将索引__index[key]的值)
end
end
__index元方法可以是函数,也可以是一个table。以下示例一个面向对象的例子,这里__index是一个函数,假设有People这个类,People里面含有name、age这些变量,含有SayHello、New这些成员方法,创建People的两个实例对象XiaoMing和LiHua,然后调用这两个实例对象中不存在的name、age、SayHello、New等字段。
输出
当__index元方法是一个table的时候,table[key]如果为nil,则会去查找__index[key]。以下示例当__index是一个table的时候的用法。
输出
如果在访问一个table时,不想触发它的__index元方法,可以使用函数rawget(t,i)来获取table中元素的值。
输出
(11)__newindex元方法。
__newindex元方法与__index元方法类似,只是__index用于索引一个table的值,而__newindex用于table的赋值,也就是table[key] = value的时候,如果table[key]不存在,则会查找__newindex元方法,如果__newindex不为nil,则调用__newindex元方法。
调用__newindex元方法的逻辑原型如下:
function settable_event (table, key, value)
local h
iftype(table) == "table" then
localv = rawget(table, key)
ifv ~= nil then rawset(table, key, value); return end
h =metatable(table).__newindex
ifh == nil then rawset(table, key, value); return end
else
h =metatable(table).__newindex
ifh == nil then
error(···)
end
end
if type(h)== "function" then
h(table,key,value) -- (如果__newindex是function,调用它)
elseh[key] = value -- (如果__newindex是table,索引__newindex[key])
end
end
以下示例当__newindex为函数时的用法。
输出
(12)__call元方法。
__call元方法是“调用”的方法,对应符号”()”,当在一个对象的后面使用调用,lua首先会判断这个对象是否是function类型,如果是则按照执行函数的形式执行这个对象;如果这个对象不是function类型,则调用__call方法。
调用__call元方法的逻辑原型如下:
function function_event (func, ...)
iftype(func) == "function" then
returnfunc(...) -- (原始的调用)
else
localh = metatable(func).__call
ifh then
returnh(func, ...)
else
error(···)
end
end
end
以下示例__call元方法的用法,实现当调用一个table时,打印它的参数。
输出
(13)__mode元方法。
__mode是lua用于实现table的弱引用的元方法(可以参考lua(5)-table(表)这篇文章),每一次lua的内存回收都会检测该table内是否含有__mode元方法,当一个table的__mode元方法被声明并定义后,内存回收将会清除table被标记为“垃圾”的对象。
以下示例__mode元方法的使用
输出