前面讲了lua的table,讲得比较粗略,不过table将会在不断的运用中逐渐形成对它的认识,不必一开始就完全搞明白它的全部。从这里开始,将讲解lua的元表和元方法。
二、元表与元方法
我们知道c++ 中不能随便将两个对象相加,除非程序用户自己定义+操作符,指定两个对象相加时需要做的操作。lua也是一样,不能将两个table进行算术操作,但是有一种方法可以实现。
元表和元方法就是用来改变lua 中元素的特定行为的。
lua中对元表的操作有如下方法:
setmetatable(t, mt) 设置 t 的元表为mt ,也可以重新设定
getmetatable(t) 获取 t 的元表,如果t 没有元表则返回nil
这里虽然说是可以对lua中元素的元表进行操作,但实际上,lua代码中只可以对table的元表进行设置,如要设置其他类型值的元表,则必须通过C API来实现。
lua中的每个值都有一个元表,table和userdata可以各自有独立的元表,而其他类型的值则共享该类型所属的单一元表。新建的table不会自动创建元表。
任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表表示他们的共同行为。一个table甚至可以作为自己的元表,用于描述特有的行为。
下面以一个元素集合为例,说明元表与元方法的应用:
localSet = {}
functionSet.New(obj)
obj = obj or {}
local tt = {}
for k,v in ipairs(obj) do
tt[v] = true
end
return tt
end
functionSet.Union(obja , objb)
local obj = Set.New()
for k in pairs(obja) do
obj[k] = true
end
for k in pairs(objb) do
obj[k] = true
end
return obj
end
functionSet.Intersesion(obja , objb)
local obj = Set.New()
for k,v in pairs(obja) do
obj[k] = objb[k]
end
return obj
end
functionSet.Print(obj)
io.write("{ ")
for k in pairs(obj) do
io.write(k.." ")
end
print("}")
end
这里用Set表示一个集合的属性tabel,定义了一个New方法,将传进来的一组值表示成一个集合,这里用一个数组来表示,以该元素值为键,值为true表示存在该元素。
obj= obj or {} 是lua中比较常见的一种写法,用于让元素obj有一个不为nil的值,当用户调用函数没有传table进来时,我们获取的obj就为nil值,这是对obj进行索引迭代就会出错。所以obj不为nil时则为本身,否则为一个空table。
Set的Union函数为处理两个集合的并集,分别将两个集合的元素都按规则填充到集合中即可。Intersection函数则处理两个集合的交集,这里用了一点下技巧,先遍历obja集合,每个k都是集合obja的元素,执行操作
obj[k]= objb[k]
则当k为objb中的元素时,objb[k]为true,所以obj[k]= true将元素k加入到结果集。
当k不为objb中的元素时,objb[k]为nil,所以执行obj[k]= nil不会将k加入到结果集。
localt1 = Set.New{1,2,3,4,5,6}
localt2 = Set.New{4,5,6,7,8,9}
Set.Print(t1)
Set.Print(t2)
输出为
{ 1 2 3 4 5 6 }
{ 4 5 6 7 8 9 }
那么,我们要实现用操作符来求两个集合的并集和交集该怎么办呢?
定义一个用作元表的table
localmet = {}
在Set的New函数中增加
setmetatable(tt, met)
将新建的集合table的元表设置为met,并设置元表两个元方法如下
met.__add= Set.Union
met.__mul= Set.Intersesion
这里用算术运算+计算并集,用*计算交集,样就可以了:
localt3 = t1 + t2
Set.Print(t3)
localt4 = t1 * t2
Set.Print(t4)
输出为
{1 2 3 4 5 6 7 8 9 }
{5 6 4 }
可以看到,t3确实是t1和t2的并集,t4确实是t1和t2的交集。
那么,table的元表又是如何运作的呢?
当lua解析器解析t1 + t2时,如果t1有元表,并且元表中有__add字段,则用t1元表的该元方法,而与t2的元表无关,只有当t1没有找到这种元方法是才会查找t2(注意如果t2的元表的__add元方法与t1不同的情形)。当t1和t2中都找不到__add元方法时,lua就会引发一个错误。
通过这个例子,我们想一下元表和元方法是什么?
我们可以把元表看成事本例中Set集合对象抽象出来的一个类对象(注意不是类,元表是table也是对象),这个对象定义了对于以它为元表的对象某些操作的行为(后面还会讲到,元对象提供的不单是方法,还有值)。
lua中只有对象,没有类,对象可以生成对象,再次强调这句话。
当对table进行索引某个方法或值时,lua首先会查找table本身是否有这样的方法或值,如果有则直接使用。否则,会查找该table是否存在元表,并在查找的元表中查找这样的方法或值。
lua中还定义了一些其他的具有通用操作符的元方法:
算术:__add(加法)、__sub(减法)、__mul(乘法)、__div(除法)、__unm(相反数)、__mod(取模)、__pow(乘幂)、__concat(字符串连接)
关系:__eq(等于)、__lt(小于)、__le(小于等于)
其他几个重要的元方法:
__tostring 可以定义将table表示为字符串的元方法
__metatable 调用setmetatable和getmetatable会用到元表的该元方法。如果手动设置元表的__metatable这个元方法的值,则会对table起到保护的作用,外部将不能访问该table的元方法,更不能调用setmetatable设置新元表。
localtt = {}
localmt = {}
mt.__metatable= "not your business"
setmetatable(tt, mt)
print(getmetatable(tt))
setmetatable(tt, {})
输出为
notyour business
lua:metatable2.lua:6: cannot change a protected metatable
__index做为lua中一个非常重要的元方法,它可以是一个函数也可以是一个table。当它为一个函数时,lua会以table和一个不存在不存在的key(存储的key则直接访问)来调用函数。如果为一个table,lua就以相同的方式来重新访问这个table。什么意思呢?来看下面的一个例子:
localwindow = {}
window.prototype= {x=10,y=20,width=10,height=10}
window.mt= {}
window.mt.__index= function(tbl , key)
return window.prototype[key]
end
functionwindow.New(obj)
obj = obj or {}
setmetatable(obj , window.mt)
return obj
end
localtb = window.New{x=20,y=30}
print(tb.x)
print(tb.width)
window表将一些属性存储在prototype中,还有一个作为元表的mt。当调用New函数生成对象tb时,设置了tb的元表为mt。当对tb索引x时,首先查找tb本身是否存在x键,发现存在则直接访问。索引width时,tb没有,lua发现它有元表,就跳转到表mt中,要访问mt必须找到__index元方法,发现是一个函数,则将tb和width作为参数传入调用。
那__index为table时又是什么情况呢,我们来做一个实验:
localwindow = {}
window.prototype= {x=10,y=20,width=20,height=20}
window.mt= {}
functionwindow.New(obj)
obj = obj or {}
setmetatable(obj , window.mt)
return obj
end
localt = window.New{x=20,y=30}
print(t.width)
这里会输出什么呢?结果是nil。对t索引width时虽然会跳转到元表window.mt上,但是lua并不知如何处理元表window.mt(缺少__index元方法)。
当我们在window.mt = {}之后加上一行:
window.mt.__index= window.prototype
输出就是20了。
也就是说,元方法__index是告诉lua对元表应该如果访问或操作。前面的例子将mt的__index元方法指向一个函数,则lua会调用这个函数。这里将mt的__index指向一个table,则lua就会去访问这个table。
当然,这种访问还会引起其他的一下操作,我们将在面向对象的时候将到。