学习Cocos2d-x Lua:实现类的原理
在这个系列中,我们将深入学习Cocos2d-x Lua,总结Lua开发过程中涉及的知识点,以及如何在开发过程中使用Cocos Code IDE。本篇文章将详细讲解在Lua中实现类的原理。
类与实例的基本概念
类是什么
Lua本身并没有类的概念,但我们可以利用其语言特性来实现类。在编程中,类可以看作是一种自定义的变量类型,它是属性和方法的集合。类中的每个方法都有一个名字,方法名和方法函数之间形成了键值映射关系,即方法名作为键,对应的方法函数作为值。
例如,我们定义一个“人”类(Person),“说话”(talk)可以作为它的一个方法名,而实现说话功能的函数则是该方法名对应的值。同时,类还可以有属性,比如“性别”(sex),“性别”作为键,其实际值就是该键所对应的内容。
基于类是键值对集合的理解,我们可以使用Lua中自带的表来实现类。
实例是什么
如果把类看作是一个键值映射的表,那么实例同样也是一个表,它具有类的属性和方法。类在全局只有一个集合,就像“上帝”一样,全局只占用一块内存;而实例则可以有多个,彼此独立分配内存。
例如,现实生活中有很多人,每个人都可以执行“说话”这个方法,但一个人的说话行为不会影响其他人。从编程角度来看,实例就是由类创建出来的值,我们可以把类想象成类型。
Lua中的语法糖
函数语法糖
我们来创建一个“人类”(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
参数会显得不够美观。因此,Lua提供了冒号语法糖。我们可以这样定义人类的“说话”功能:
function Person:talk(words)
print(self.name.."说:"..words)
end
这段代码与前面两段代码等价,它的变化是少了self
参数,将点号Person.talk
改为了冒号Person:talk
。在函数体内,依然可以使用self
,当使用冒号:
代替点号.
时,Lua会自动将self
作为第一个参数,self
代表的是函数的实际调用者。所以,Person:talk("你好")
与Person.talk(Person, "你好")
是等价的。
Lua表中元素的查找机制
在Lua中,我们需要了解如何在表中查找一个键所对应的值。假设我们要在表p
中查找talk
这个键所对应的值,理解这个查找过程是本文的重点。
由于metatable
(元表)和__index
这两个特性,Lua能在当前表中不存在该键时找到其返回值。下面我们将详细介绍metatable
这个语言特性。
深入理解metatable
元表的定义
metatable
的中文名是元表,它本质上也是一个表。在Lua中,表的操作是有限的,例如表不能直接相加、不能进行比较操作等。元表的作用就是增加和改变表的既定操作,只有设置过元表的表,才会受到元表的影响而改变自身的行为。
我们可以通过全局方法setmetatable(t, m)
将表t
的元表设置为表m
,使用getmetatable(t)
则可以返回表t
的元表m
。需要注意的是,所有的表都可以设置元表,但新创建的空表如果不设置,是没有元表的。
元方法
元表作为一个表,可以拥有任意类型的键值对,但真正对被设置的表产生影响的是Lua规定的元方法键值对。这些键名通常以双下划线__
为前缀,如__index
、__add
、__concat
等,其对应的值为一个函数,被称为元方法(metamethod),这些元方法定义了我们想对表自定义的操作。
__index
元方法
__index
键对应的元方法在查找不存在于表中的键时会被执行。以下是一个示例代码:
-- 定义元表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
这个键,但通过设置其元表为m
,并为m
的__index
设置对应的方法,所有取不到的键都会返回“undefined”。这表明元表的__index
属性实际上为表配备了找不到键时的行为。需要注意的是,元表的__index
属性对应的也可以是一个表。
__add
元方法
再以__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
-- 输出
-- 1 23
-- 2 25
-- 3 27
表本身不能使用+
进行计算,但通过定义元表的__add
方法,并将其设置到需要进行加法操作的表上,这些表就可以进行加法运算了。这说明元表的__add
属性为表定义了使用+
号时的行为。
Lua中类的实现
在了解了前面的内容后,我们来实现一个Lua类。我们的类是一个表,它定义了各种属性和方法;实例也是一个表,我们将类作为元表设置到实例上,并将类的__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
类创建出多个实例,每个实例都具备Person
类的属性和方法。
至于类的继承,大家可以将其当作一次练习进行思考。