表:Lua中的数据结构,可以存放各种域。
由于Lua中的函数也是第一类值,于是表可以存放属性域和方法域,因此表可以描述一个类。
元表:元表也是一个表,可以设置给一个本表。
一个表作为元表概念存在的意义在于它可以通过定义自身的一系列元方法来改变本表的行为。
如果一个元表不被设置给一个本表,那么他是没有意义的,一个本表的元表可以是自身。
元方法:Lua给表预备了一些特殊的行为(如表之间的相加/减/乘/除等),这些行为可以通过元方法来定义。
一个表的元方法需要定义在它的元表中,一个没有元表的表无法使用任何元方法。
如果一个表的元表是自身,那么在该表中定义的元方法将影响该表的行为。
__index元方法:当访问到表中不存在的域时,将调用其元表的__index元方法(如果该表指定了元表并且元表定义了__index元方法的话)。如果其元表的__index方法返回一个表中含有该域,则访问该域。
表,元表,元方法的功能组合起来,可以让lua代码实现面向对象的机制:
封装:
将具备相同性质的一类事物抽象成一组属性域和方法域,在C++中这个概念叫做类。在Lua中没有类的概念,而是用一个表来存储这一组属性域和方法域。
通过对类的实例化可以产生可以操作的对象,但是在Lua中没有实例化的概念,Lua使用“原型”的思路来实现对象的实例化:
一个原型表用来描述一个类,在原型表中填写类属性和方法。
实例化对象时,创建一个新表,将新表的元表设置为原型表。将原型表的__index指向自身。
于是,所有使用该方法产生的新表都将拥有相同的元表,相当于所有的新表都可以访问到相同的域:
Base = {x = 0 , y = 0};
Base.new = function(self)
local o = {}; --创建新表
setmetatable(o,self); --新表的元表设置为原型表
self.__index = self; --原型表的__index为原型表
return o;
end
Base.setXY = function (self,x,y)
self.x = x;
self.y = y;
end
b1 = Base:new();
b1:setXY(666,999);
b2 = Base:new();
b2:setXY(333,999);
print(b1.x); --'666'
这样看起来一切正常,b1和b2的元表都是Base。
访问b1,b2中不存在的域setXY会通过元表Base.__index返回Base.setXY。
但是当b1生成的时候他的x域根本不存在,直到b1:setXY(b1,666,999)的时候通过一个赋值才生成的x。
这个x在表b1中,各个实例之间互不影响,但是在多个实例访问Base中的表成员数据时会产生错误:
Base = {x = 0 , y = 0,
t = { h = 0 };
};
b1 = Base:new();
b1.t.h = 100;
b2 = Base:new();
print(b2.t.h); --'100'
t不在对象表中,而在原型表Base中,通过__index访问到了类中的数据,看起来像个static成员。
于是,类中的成员最好声明在构造函数内的新表中,而方法可以声明在原型表中(由于在调用的时候会传入self引用)。
Base = {};
Base.new = function(self)
local o = {
x = 0 , y = 0,
t= { h = 0 },
};
setmetatable(o,self);
self.__index = self;
return o;
end
这样当b1,b2被创建的时候就存在xy和t域。
继承:
Lua中的继承很有趣,派生一个类只需要一个原型表的新实例:
Deriver = Base:new();
Deriver表现在看起来是一个Base原型表的新实例,它的元表是Base
重写Deriver的new方法,在new方法中扩充Deriver的新属性:
Deriver.new = function(self)
local o = getmetatable(self).new(self);
o.z = 999;
return o;
end
dobj1 = Deriver:new();
dobj1:setXY(1,2);
print(dobj1.x,dobjdobj1.z); -- 1 2 999
Deriver实例化的新对象dobj1实际上为表o,o表带有Deriver原型表中定义的所有属性,
由于实例化时Deriver.new传入的self参数是Deriver,因此o的元表是Deriver,Deriver的元表是Base,这样构成了一个继承链。
当派生表中定义了和原型表相同的域时,会优先执行派生表中的域,因此方法重载和多态性是天然产生的。