最近一周简单学习了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继承来的方法。