最近一周简单学习了lua的基础内容,本文主要介绍了lua中metatable(元表),并利用metatable实现了lua中的继承。
一、元表
首先看什么是元表,元表本身只是一张普通的表,一般带有一些特殊的事件回调函数,通过 setmetatable被设置到某个对象上进而影响这个对象的行为。
元表有什么用呢,用书中的一句话说就是“可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作”。在C++中,两个对象是无法直接相加的,但是,如果一个类重载了“+”运算符,它的对象就可以进行加法运算。在Lua中也类似,两个table类型的变量,是无法直接进行“+”操作的,如果定义了一个指定的函数,就可以进行了。接下来就介绍如何定义这个指定的函数。
有两个表t1和t2,计算t1+t2的步骤是:
首先判断t1或者t2有没有元表
然后查找t1或者t2的元表中是否有__add字段
最后调用t1或t2元表中__add字段对应的函数
将一个表t的元表设置为mt的方法是setmetatable(t,mt),获取一个表t的元表的方法是getmetatable(t).
下面通过一个简单的例子进行演示,给出两个表
t1 = {1, 2, 3}
t2 = {3, 4}
以及我们打算当做元表使用的表
mt = {}
在没有设置元表的时候
size = t1 + t2
会报错,因为表t1和t2都没有定义“+”这个操作
我们将mt设为t1的元表
setmetable(t1, mt)
然后在元表mt中加入__add字段及其对应的函数
mt.__add = function(a, b)
return #a + #b
end
我定义的这个函数很简单,只是返回两个table长度的和,可以根据实际的需要来写。
现在,我们就可以这样写了
print(t1 + t2)
这样,我们就实现了自己定义的table的加法运算。
在table中,我可以重新定义的元方法有以下几个:
__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表
最后一个字段__metatable用于保护元表,当设置了该字段的时候,getmetatable就会返回这个字段对应的值,而setmetatable会引发一个错误。
下面介绍关于访问的两个元方法:__index元方法和__newindex元方法。
当访问一个table中不存在的字段时,如果这个table的元表没有设置__index字段,那么会得到nil,而当设置了__index字段时,就由这个字段对应的元方法来提供最终结果。
举个简单的例子,现在有个一table
point2d = {x = 10, y = 20}
当访问这个table中不存在的字段时,会得到nil
print(point2d.z)
接下来为point2d设置元表,并定义元表中__index字段
mt.__index = function (table, key)
return 0
end
setmetatable(point2d, mt)
我在这个方法中定义访问未定义的字段时返回0,因此point2d[z]的值就是0.
print(point2d.z) -->0
__index的元方法可以是一个函数,也可以是一个table,当它是一个函数的时候,Lua以table和不存在的key为参数来调用该函数,而当它是一个table的时候,Lua会以相同的方式来重新访问这个table。
因此,在进行如下的设置之后,point2d.z的值就变为-5
point3d = {x = 8, y = 10 , z = -5}
mt.__index = point3d
print(point2d.z)
__newindex元方法与__index元方法类似,它用于table的更新,当对一个table中不存在的索引赋值时,如果它的元表中有__newindex这个元方法,解释器就会调用它,而不是执行赋值,如果这个元方法是一个table,解释器就在此table中执行赋值。
接上例,执行如下的操作
mt.__newindex = point3d
point2d.z = 5
print(point3d.z) -->5
可以看到,通过对point2d中不存在的索引赋值,改变了__newindex元方法对应table的值。
二、在lua中实现继承
接下来我们通过__index元方法实现Lua中的单继承。
Lua 没有类的概念,不过可以通过元表来实现与原型(prototype)类似的功能。
Point2d = {x = 0, y = 0}
function Point2d.show(self)
print("("..self.x..","..self.y..")")
end
function Point2d:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
p2d = Point2d:new()
p2d:show() -->(0,0)
p2d = Point2d:new({y = 2, x = 1})
p2d:show() -->(1,2)
Point3d = Point2d:new({z = 0})
function Point3d.show(self)
print("("..self.x..","..self.y..","..self.z..")")
end
p3d = Point3d:new()
p3d:show() -->(0,0,0)
p3d = Point3d:new({x = 1})
p3d:show() -->(1,0,0)
p3d= Point3d:new({x = 1, y = 2, z = 3})
p3d:show() -->(1,2,3)
我们把Point2d看做一个类,它有两个属性x和y以及两个方法new()和show(),通过执行p2d = Point2d:new(),使p2d成为它的一个实例,当调用函数p2d.show()时,首先在p2d中查找show字段,没有找到,所以会在它的元表中查找__index字段,该字段对应的值是Point2d,因此会在Point2d中查找show字段,最后调用该字段对应的函数,在打印self.x和self.y时,self是p2d,而p2d中没有字段x,因此会在查看它的原表的__index字段,该字段对应的值是Point2d,所以会在Point2d中查找x字段,最后返回0,self.y同理。
Point3d是Point2d的一个实例,它继承了Point2d的属性和方法,然后添加了z这个属性,为了打印z这个属性,我们可以重写从Point2d继承来的函数show(),在执行p3d = Point3d.new()时,Point3d中没有new字段,所以会在它的元表Point2d的__index字段对应的Point2d中查找,最终p3d的元表和元表中__index字段对应的值都是Point3d。在执行p3d.show()的时候,p3d中没有show()字段,会在在Point3d中查找show()字段。当需要求值p3d.x时,p3d中没有该字段,会在Point3d进行查找,Point3d中仍然没有该字段,Point3d的元表中__index字段对应的值为Piont2d,最终在Point2d中查找到该字段的值为0.对p3d.y的求值同p3d.x,而对p3d.z求值时,可以Point3d中找到字段z。
从面向对象的角度来看,Point3d继承自Point2d,拥有了Point2d的属性和方法,同时它又可以重写从Point2d继承来的方法。