Lua中的metatable详解
Lua中metatable是一个普通的table,但其主要有以下几个功能:
1.定义算术操作符和关系操作符的行为
2.为Lua函数库提供支持
3.控制对table的访问
Metatables定义操作符行为
Metatable能够被用于定义算术操作符和关系操作符的行为。例如:Lua尝试对两个table进行加操作时,它会按顺序检查这两个table中是否有一个存在metatable并且这个metatable是否存在__add域,如果Lua检查到了这个__add域,那么会调用它,这个域被叫做metamethod。
Lua中每个value都可以有一个metatable(在Lua5.0只有table和userdata能够存在metatable)。每个table和userdatavalue都有一个属于自己的metatable,而其他每种类型的所有value共享一个属于本类型的metatable。在Lua代码中,通过调用setmetatable来设置且只能设置table的metatable,在C/C++中调用LuaCAPI则可以设置所有value的metatable。默认的情况下,string类型有自己的metatable,而其他类型则没有:
print(getmetatable('hi')) -->table:003C86B8 print(getmetatable(10)) -->nil
Metamethod的参数为操作数(operands),例如:
localmt={} functionmt.__add(a,b) return'table+'..b end localt={} setmetatable(t,mt) print(t+1)
每个算术操作符有对应的metamethod:
+
__add
*
__mul
-
__sub
/
__div
-
__unm(fornegation)
%
__mod
^
__pow
对于连接操作符有对应的metamethod:__concat
同样,对于关系操作符也都有对应的metamethod:
==
__eq
<
__lt
<=
__le
其他的关系操作符都是用上面三种表示:
a~=b表示为not(a==b)
a>b表示为b<a
a>=b表示为b<=a
和算术运算符不同的是,关系运算符用于比较拥有不同的metamethod(而非metatable)的两个value时会产生错误,例外是比较运算符,拥有不同的metamethod的两个value比较的结果是false。
不过要注意的是,在整数类型的比较中a<=b可以被转换为not(b<a),但是如果某类型的所有元素并未适当排序,此条件则不一定成立。例如:浮点数中NaN(NotaNumber)表示一个未定义的值,NaN<=x总是为false并且x<NaN也总为false。
为Lua函数库提供支持
Lua库可以定义和使用的metamethod来完成一些特定的操作,一个典型的例子是LuaBase库中tostring函数(print函数会调用此函数进行输出)会检查并调用__tostringmetamethod:
localmt={} mt.__tostring=function(t) return'{'..table.concat(t,',')..'}' end localt={1,2,3} print(t) setmetatable(t,mt) print(t)
另外一个例子是setmetatable和getmetatable函数,它们定义和使用了__metatable域。如果你希望设定的value的metatable不被修改,那么可以在value的metatable中设置__metatable域,getmetatable将返回此域,而setmetatable则会产生一个错误:
mt.__metatable="notyourbusiness" localt={} setmetatable(t,mt) print(getmetatable(t))-->notyourbusiness setmetatable(t,{}) stdin:1:cannotchangeprotectedmetatable
看一个完整的例子:
Set={} localmt={} functionSet.new(l) localset={} --为Set设置metatable setmetatable(set,mt) for_,vinipairs(l)doset[v]=trueend returnset end functionSet.union(a,b) --检查ab是否都是Set ifgetmetatable(a)~=mtorgetmetatable(b)~=mtthen --error的第二个参数为level --level指定了如何获取错误的位置 --level值为1表示错误的位置为error函数被调用的位置 --level值为2表示错误的位置为调用error的函数被调用的地方 error("attemptto'add'asetwithanot-setvalue",2) end localres=Set.new{} forkinpairs(a)dores[k]=trueend forkinpairs(b)dores[k]=trueend returnres end functionSet.intersection(a,b) localres=Set.new{} forkinpairs(a)do res[k]=b[k] end returnres end mt.__add=Set.union mt.__mul=Set.intersection mt.__tostring=function(s) locall={} foreinpairs(s)do l[#l+1]=e end return'{'..table.concat(l,',')..'}' end mt.__le=function(a,b) forkinpairs(a)do ifnotb[k]thenreturnfalseend end returntrue end mt.__lt=function(a,b) returna<=bandnot(b<=a) end mt.__eq=function(a,b) returna<=bandb<=a end locals1=Set.new({1,2,3}) locals2=Set.new({4,5,6}) print(s1+s2) print(s1~=s2)
控制table的访问
__indexmetamethod
在我们访问table的不存在的域时,Lua会尝试调用__indexmetamethod。__indexmetamethod接受两个参数table和key:
localmt={} mt.__index=function(table,key) print('table--'..tostring(table)) print('key--'..key) end localt={} setmetatable(t,mt) localv=t.a
__index域也可以是一个table,那么Lua会尝试在__indextable中访问对应的域:
localmt={} mt.__index={ a='HelloWorld' } localt={} setmetatable(t,mt) print(t.a)-->HelloWorld
我们通过__index可以容易的实现单继承(类似于JavaScrpit通过prototype实现单继承),如果__index是一个函数,则可以实现更加复杂的功能:多重继承、caching等。我们可以通过rawget(t,i)来访问tablet的域i,而不会访问__indexmetamethod,注意的是,不要太指望通过rawget来提高对table的访问速度(Lua中函数的调用开销远远大于对表的访问的开销)。
__newindexmetamethod
如果对table的一个不存在的域赋值时,Lua将检查__newindexmetamethod:
1.如果__newindex为函数,Lua将调用函数而不是进行赋值
2.如果__newindex为一个table,Lua将对此table进行赋值
如果__newindex为一个函数,它可以接受三个参数tablekeyvalue。如果希望忽略__newindex方法对table的域进行赋值,可以调用rawset(t,k,v)
结合__index和__newindex可以实现很多功能,例如:
1.OOP
2.Read-onlytable
3.Tableswithdefaultvalues
Read-onlytable
functionreadOnly(t) localproxy={} localmt={ __index=t, __newindex=function(t,k,v) error('attempttoupdatearead-onlytable',2) end } setmetatable(proxy,mt) returnproxy end days=readOnly{'Sun','Mon','Tues','Wed','Thur','Fri','Sat'} print(days[1]) days[2]='Noday'-->stdin:1:attempttoupdatearead-onlytable
有时候,我们需要为table设定一个唯一的key,那么可以使用这样的技巧:
localkey={}--uniquekey localt={} t[key]=value