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