Lua中没有类的概念,但我们可以利用Lua本身的语言特性来实现类。
下文将详细的解释在Lua中实现类的原理,涉及到的细节点将拆分出来讲,相信对Lua中实现类的理解有困难的同学将会释疑。
类是什么?
想要实现类,就要知道类到底是什么。
在我看来,类,就是一个自己定义的变量类型。它约定了一些它的属性和方法,是属性和方法的一个集合。
所有的方法都需要一个名字,即使是匿名函数实际上也有个名字。这就形成了方法名和方法函数的键值映射关系,即方法名为键,映射的值为方法函数。
比如说有一个类是人,人有一个说话的方法,那就相当于,人(Person)是一个类,说话(talk)是它的一个方法名,说话函数是它的实际说话所执行到的内容。
人也有一个属性,比如性别,性别就是一个键(sex),性别的实际值就是这个键所对应的内容。
理解了类实际上是一个键值对的集合,我们不难想到用Lua中自带的表来实现类。
实例是什么?
如果理解了类实际就是一个键值映射的表,那么我们再来理解实例是什么。
实例就是具有类的属性和方法的集合,也是一个表了。听起来好像和类差不多?
类全局只有一个集合,相当于上帝,全局只有一块内存;而实例就普通了,普天之下有那么多人,你可以叫A说一句话,A便执行了他的说话方法,但是不会影响B的说话。因为他们是实例,彼此分配着不同的内存。
说了那么多废话,其实实例就是由类创建出来的值,试着把类想象成类型而不是类。
两个语法糖
试着创建一个人类 Person
复制代码 代码如下:
Person = {name="这个人很懒"}
以上代码将Person初始化为一个表,这个表拥有一个为name的键,其默认值是"这个人很懒"。
说成白话就是人类拥有一个叫名字的属性。
那就再赋予人类一个说话的功能吧。
复制代码 代码如下:
Person.talk = function(self, words)
print(self.name.."说:"..words)
end
以上代码在Person表中加入一个键值对,键为talk,值为一个函数。
好了,只要调用,Person.talk(Person, "你好"),将会打印出:这个人很懒说:你好。
不过在写程序时,大家都习惯把function放在前面,这就是函数的语法糖:
复制代码 代码如下:
function Person.talk(self, words)
print(self.name.."说:"..words)
end
这与上面的函数定义是等价的,但是这么写你就很难看出来talk其实是Person表中的一个键,其对应的值为一个函数。
当然嘴巴都是长在自己身上的,说话只能自己说,不可能自己张嘴别人说话,所以每次都传个self参数实在是有点不美观,于是冒号语法糖上场。
我们还可以这么定义人类的说话功能:
复制代码 代码如下:
function Person:talk(words)
print(self.name.."说:"..words)
end
这与上面两段代码都是等价的,它的变化是少了self的参数,将点Person.talk改为了冒号Person:talk。
但是函数体内,却依然可以使用self,在使用:代替.时,函数的参数列表的第一个参数不再是words,Lua会自动将self做为第一个参数。这个self参数代表的意思就是这个函数的实际调用者。
所以我们调用Person:talk("你好")与Person.talk(Person, "你好")是等价的,这就是冒号语法糖带来的便利。
如何查找表中的元素?
下面我们需要理解在Lua的表中是怎么查找一个键所对应的值的。
假设我们要在表p中查找talk这个键所对应的值,请看下面的流程图:
复制代码 代码如下:
p中有没有talk这个键"codetitle">复制代码 代码如下:
--定义元表m
m = {}
--定义元表的__index的元方法
--对任何找不到的键,都会返回"undefined"
m.__index = function ( table, key )
return "undefined"
end
--表pos
pos = {x=1, y=2}
--初始没有元表,所以没有定义找不到的行为
--因为z不在pos中,所以直接返回nil
print(pos.z) -- nil
--将pos的元表设为m
setmetatable(pos, m)
--这是虽然pos里仍然找不到z,但是因为pos有元表,
--而且元表有__index属性,所以执行其对应的元方法,返回“undefined”
print(pos.z) -- undefined
pos表中本没有z这个键,通过设置pos的元表为m,并设置m的__index对应的方法,这样所有取不到的键都会返回“undefined”了。
以上我们了解到,元表的__index属性实际上是给表配备了找不到键时的行为。
注意:元表的__index属性对应的也可以为一个表。
再举个栗子,希望能够加深对元表和元方法的理解,__add键,考虑以下代码:
复制代码 代码如下:
--创建元表m,其中有__add键和其定义的方法
local m = {
__add = function(t1, t2)
local sum = {}
for key, value in pairs(t1) do
sum[key] = value
end
for key, value in pairs(t2) do
if sum[key] then
sum[key] = sum[key] + value
else
sum[key] = value
end
end
return sum
end
}
--将table1和table2都设置为m
local table1 = setmetatable({10, 11, 12}, m)
local table2 = setmetatable({13, 14, 15}, m)
--表本来是不能执行 + 操作的,但是通过元表,我们做到了!
for k, v in pairs(table1 + table2) do
print(k, v)
end
--print
--1 23
--2 25
--3 27
表本身是不能用+连起来计算的,但是通过定义元表的__add的方法,并setmetatable到希望有此操作的表上去,那些表便能进行加法操作了。
因为元表的__add属性是给表定义了使用+号时的行为。
类的实现手段
好,假设前面的内容你都没有疑问的阅读完毕话,我们开始进入正题。
请先独立思考一会,我们该怎么去实现一个Lua的类?
思考ing…
种种铺垫后,我们的类是一个表,它定义了各种属性和方法。我们的实例也是一个表,然后我们类作为一个元表设置到实例上,并设置类的__index值为自身。
例如人类:
复制代码 代码如下:
--设置Person的__index为自身
Person.__index = Person
--p是一个实例
local p = {}
--p的元表设置为Person
setmetatable(p, Person)
p.name = "路人甲"
--p本来是一个空表,没有talk这个键
--但是p有元表,并且元表的__index属性为一个表Person
--而Person里面有talk这个键,于是便执行了Person的talk函数
--默认参数self是调用者p,p的name属性为“路人甲”
p:talk("我是路人甲")
--于是得到输出
--路人甲说:我是路人甲
为了方便,我们给人类一个创建函数create:
复制代码 代码如下:
function Person:create(name)
local p = {}
setmetatable(p, Person)
p.name = name
return p
end
local pa = Person:create("路人甲")
local pb = Person:create("路人乙")
pa:talk("我是路人甲") --路人甲说:我是路人甲
pb:talk("我是路人乙") --路人乙说:我是路人乙
这样我们可以很方便用Person类创建出pa和pb两个实例,这两个实例都具备Person的属性和方法。
以上便是Lua实现一个类的方法,至于类的继承,当成一次练习吧,请大家思考~