学习Cocos2d-x Lua:实现类的原理

2015年03月25日 11:22 0 点赞 0 评论 更新于 2017-05-09 19:33

在这个系列中,我们将深入学习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类的属性和方法。

至于类的继承,大家可以将其当作一次练习进行思考。

作者信息

boke

boke

共发布了 1025 篇文章