Lua学习笔记--面向对象

一.引言

最近又开始折腾Lua了,说实话以前没有把Lua看成什么高深的语言,一直把他当做是配合宿主程序的所谓的“脚本”(虽然事实的确如此),不过最近看了一些Lua代码才发现,原来Lua可以通过很简单巧妙的变化,模拟出一些其他语言引以为傲的特性,真没想到Lua还可以这样玩,哈哈。

目前主流的编程语言C++/C#/Java都是面向对象的语言,面向对象符合我们正常的思维观念,面向对象的特性,封装,继承等更是可以大大的帮助我们提升编程的效率。比如我们要写一系列的人物对象,每个对象的基础方法都相同,只是有一些特有的方法和字段,如果没有面向对象,我们可能就要“Ctrl+C”,"Ctrl+V"了,这可是写代码的大忌啊。所以机智的先人们早就想好了怎么让Lua面向对象了。

不过在学习怎么让Lua面向对象之前,还要学习一点预备知识,因为这个东东才是让Lua能够面向对象的秘密武器。


二.元表&元方法

1.元表(metatable)简介

    元表是Lua中很重要的一个概念,所谓元表(metatable),就是对应一个类型最基本的一些方法的集合,这些方法叫做元方法(metamethod)。
举个例子来说明一下。比如+方法,我们给两个数进行加法运算,系统就会自己调用数的__Add元方法进行加法操作。我们并没有定义这个方法,
但是Lua会去数的元表里找到_Add进行加法运算。
    Lua中,我们定义一个table时,是不会自动生成元表的。

_G.Base = {}

Base.name = "Base"

function Test()
    print(getmetatable(Base));
end

Test()
结果:

nil

我们可以通过setmetatable方法来给table设置元表。看一个例子:

Set = {}

--创建新的Set
function Set.new(l)
	local set = {}
	for _,v in ipairs(l) do
	set[v] = true
	end
	return set
end

--打印传入的Set
function Set.Print(s)
	for e in pairs(s) do
	print(tostring(e))
	end
end

--Union方法,合并两个Set
function Set.Union(a,b)
	local newSet = {}
	for e in pairs(a) do newSet[e] = true end
	for e in pairs(b) do newSet[e] = true end
	return newSet;
end

s1 = Set.new{1,2,3}
s2 = Set.new{4,5,6}
Set.Print(s1) --输出1,2,3
Set.Print(s2) --输出4,5,6
newS = Set.Union(s1,s2)
Set.Print(newS) --输出1,2,3,4,5,6


	
这是一个合并两个集合的代码,调用方式比较麻烦,有没有类似C++运算符重载的那种操作呢?这就要用到传说中的元表了。只要加上这样几句话,我们就可以实现“+”运算符把集合取并集的功能啦:

--元表
local mt = {}
--元表的Add方法(+运算符)设置为Set.Union方法
mt.__add = Set.Union

--创建新的Set
function Set.new(l)
	local s = {}
	--在要返回的s上设置元表
	setmetatable(s, mt)
	for _,v in ipairs(l) do
	s[v] = true
	end
	return s
end

--可以这样调用啦
newSet = s1 + s2
Set.Print(newSet) --输出1,2,3,4,5,6

当然,元方法不止这么多,类似的,我们还可以实现__sub,__tostring,__index等等元方法。

2.__index元方法

有这么多元方法,不过还是要说一个跟面向对象关系最密切的元方法,__index元方法。这个方法是干什么的呢?
我们知道,lua所有的东西都存储在表中,当我们访问一个存在表中的key-value对时,如果有这个key-value对,那么会返回value。但是如果不存在,就会返回nil。话是这么说,不过这中间还我们还忽略了一个过程,lua在找不到这个key-value对之后并不会立刻返回nil,而是会去找这个表有木有一个叫做__index的元方法,如果index元方法有值,那么会先返回__index元方法所指的内容,如过没有的话,才返回nil。
总结一下Lua查找table中值的过程如下:
1)查找table中对应key的字段,有则返回,没有进行下一步;
2)查找table是否有元表,元表中是否有__index元方法,如果有返回元方法的值,没有进行下一步;
3)如果__index是一个值,或者函数,返回该值或函数,如果是nil,返回nil,如果是一个表,进行下一步;
4)在__index指向的这个表中继续1)2)3)步骤。

好了,为了验证这个结论,我们做一个小实验:

--直接输出
Set1 = {}
print(Set1.hello); --输出nil


--只设置元表
Set2 = {}
Set2.mt2 = {}

function Set2.new()
	local o = {}
	setmetatable(o, Set2.mt2)
	return o
end

so2 = Set2.new()
print(so2.hello) --虽然设置了元表,但是没有设置__index元方法,所以还是nil
	

--设置元表和__index元方法
Set3 = {}
Set3.mt3 = {}

function Set3.func()
	return "I am meta method!"
end
Set3.mt3.__index = Set3.func

function Set3.new()
	local o = {}
	setmetatable(o,Set3.mt3)
	return o
end

so3 = Set3.new()
print(so3.hello) --输出I am meta method! 有元方法__index,就返回index所指向的内容

--设置元表和__index元方法,此处__index指向了一个含有hello的表
Set4 = {}
Set4.mt4 = {}

Set4Base = {}
Set4Base.hello = "I am base, I have hello"

Set4.mt4.__index = Set4Base

function Set4.new()
	local o = {}
	setmetatable(o,Set4.mt4)
	return o
end

so4 = Set4.new()
print(so4.hello) --输出I am base, I have hello 有元方法__index,且index指向了一个表,在这个表中找到了hello字段

通过上面的实验,我们看出,__index元方法确实比较强大,可以让我们在找不到内容的时候将内容指向我们希望返回的函数,值,或者table。


三.面向对象

1.重要关键字:self

所谓面向对象,就是一些数据和方法被封装在了对象里面。lua能不能做到这一点呢?想想table,我们在一个table里面定义字段,方法,那么这个table不就相当于对象了吗?并且Lua为我们提供了一个面向对象最关键的关键字self,相当于C++中的this指针。
关于self,我们可以先看一下在table中定义的lua函数:

Set = {}
Set.hello = "1"
Set.world = "2"

--这个s参数就是要被操作的Set对象
function Set.Get1(s, key)
	return s[key]
end

--不过Lua还有另外一种方式,简化了函数的定义和调用
--这种方式使用:来定义和调用函数。相当于把对象本身传递进去
function Set:Get2(key)
	return self[key] 
end



--把对象本身也传递进去
print(Set.Get1(Set, "hello")) --输出1
--省略了对象自己,有木有像C++那样默认传递this指针,注意要用:调用!
print(Set:Get2("hello")) --输出1
看到了self倍感亲切,尤其是跟C++省略this指针一样的使用方式。看来lua也专门为了面向对象做了些工作呢。

2.怎么创建对象

既然我们把方法和函数都放到了一个table里面了,这个table就相当于一个对象了。但是,但是!只有一个啊!那么我们怎么创建一个类的多个实例呢?对,没有对象就要new一个,只不过这个new要我们自己实现:

Set = {}
--传说中的new方法,自己创建一个local对象返回回去就是新对象啦!
function Set:new(name)
	local o = {}
	o.name = name
	return o
end



set1 = Set:new("set1")
set2 = Set:new("set2")
print(set1.name)--输出set1
print(set2.name)--输出set2
我们看到,通过我们Set类的new方法,创建了两个不同的对象,每个对象内部都封装了自己的name字段。

3.继承

还是上面的例子,我们想要打印一下name,那么我们直接在Set上写一个printName的方法,是不是可以打印对象的Name字段了呢?我们试一下:

Set = {}
--传说中的new方法,自己创建一个local对象返回回去就是新对象啦!
function Set:new(name)
	local o = {}
	o.name = name
	return o
end

--把对象本身传入的方法
function Set.printName1(s)
	print(s.name)
end

--使用self省略对象本身的方法
function Set:printName2()
	print(self.name)
end

set1 = Set:new("set1")
set2 = Set:new("set2")
--直接使用Set方法调用(有木有点像C++的Static方法?)
Set.printName1(set1) --输出set1
--下面的会报错
--set1.printName1(set1)--找不到printName1方法
--set2.printName1(set2)--找不到printName1方法
--set1:printName2()--找不到printName2方法
--set2:printName2()--找不到printName2方法
--没有定义Set对象本身的name字段,所以是nil
Set:printName2()   --nil
HOHO,好像失败了呢!除了使用Set对象自身的printName方法把并且把set1对象自己穿进去的那个示例,其他的都挂掉了。我们分析一下,为什么会酱紫。
Set.printName1和Set.printName2都是定义在Set类(姑且称之为类吧,其实它也是个对象)上的,而我们使用set1,set2来调的时候,这两个对象并没有定义这个方法,所以当然找不到了。最后,Set.printName2虽然调用成功了(肯定成功了,函数就是定义在它上面的),但是值却是nil。因为Set对象并没有定义name字段。
那么,那么,我们难道要给set1和set2分别定义一个printName方法吗?答案肯定是不用啦,不然我们这么半天不是白忙活了...
下面进入正题!怎么用Lua实现继承!

上一大节说到,__index元方法如果指向了一个table,在表本身找不到相应字段的时候,就会去该table中找。想想看,这个table像什么?对了,基类!我们把子类的元表的__index方法设置成基类的表。这样,在我们找一个方法找不到的时候,就会去相应的基类去寻找,这正是面向对象继承的特性。

废话多说,上代码:


Set = {}

--Set类的元表mt
Set.mt = {}
--设置元表的__index元方法为Set本身,这样在子类找不到方法定义时,会到Set类去找方法定义
Set.mt.__index = Set


--传说中的new方法,自己创建一个local对象返回回去就是新对象啦!
function Set:new(name)
	local o = {}
	o.name = name
	--给new粗来的对象设置元表
	setmetatable(o, Set.mt)
	return o
end

--把对象本身传入的方法
function Set.printName1(s)
	print(s.name)
end

--使用self省略对象本身的方法
function Set:printName2()
	print(self.name)
end

set1 = Set:new("set1")
set2 = Set:new("set2")
--直接使用Set方法调用(有木有点像C++的Static方法?)
Set.printName1(set1) --输出set1
--下面的会报错
set1.printName1(set1)--set1
set2.printName1(set2)--set2
set1:printName2()--set1
set2:printName2()--set2
--没有定义Set对象本身的name字段,所以是nil
Set:printName2()   --nil

set1,set2对象虽然没有定义printName1,2方法,但是他们设置了元表,元表的__index方法设置了找不到方法时指向的Set对象。调用时如果本类中没有该方法,就会去基类Set中找这个方法,这样就实现了继承!!

我们如果不想要基类的方法,也可以覆盖掉这个方法:

set1 = Set:new("set1")
set2 = Set:new("set2")

--覆写set1对象的printName2方法,注意:要在set1创建之后!不然会找不到set1的
function set1:printName2()
	print("haha")
end

--会调用被覆写了的函数
set1:printName2()--haha

四.总结

关于Lua面向对象暂时就说这么多。总结一下个人的一点点想法:Lua所谓的面向对象并不是真正的面向对象,而是方便我们实现代码复用的一种方式。因为一般一个类只有一个对象,子类对象继承基类的固有方法,实现一些自己的方法,覆写一些方法就可以了。
当然,还有一些更高级的应用,比如多重继承,字段的封装,可能我不知道,也欢迎大家指正。





你可能感兴趣的:(Lua)