三、lua面向对象
1、lua中的self
首先来看一个例子:
localAccount = {balance = 1000}
functionAccount.Withdraw(money)
Account.balance = Account.balance - money
end
localaa = Account;
aa.Withdraw(100)
print(aa.balance) --900
print(Account.balance) --900
Account= nil
--aa.Withdraw(100) --error : attempt to index upvalue 'Account'(a nil value)
当定义aa时,aa实际上是Account的一个“引用”,并不会实际的给aa分配该table的一个拷贝,类似于linux的硬链接或windows的内核对象,执行了Account= nil 之后,实际上这个table还在,只是Account对这个table的链接断裂,我们可以通过输出的两个值可以看出。
把Account 置为 nil 之后再调用Withdraw 函数,由于函数内使用了Account对象,所以会引起访问出错。
在看一个改进版本:
localAccount = {balance = 1000}
functionAccount.Withdraw(self,money)
self.balance = self.balance - money
end
localaa = Account
Account= nil
aa.Withdraw(aa, 100)
print(aa.balance)
aa:Withdraw(100)
print(aa.balance)
在这个版本中,调用Withdraw函数时,使用的是传入函数的self的balance值,这个self现在就是aa的“引用”了。Lua中提供了一个语法糖,使用冒号调用函数时,会把调用对象作为第一个参数传入函数。
请注意函数书写的三种方式:
(1) table中写函数,需要指定self参数:
local Account =
{
Withdraw = function(self , money)
end,
}
(2) 函数外部写函数,不指定self参数:
local Account = {}
functionAccount:Withdraw(money)
end
(3)函数外部写函数,指定self参数
local Account = {}
functionAccount.Withdraw(self , money)
end
虽然这里访问可以了,但是,这并不是面向对象,因为对aa中balance的修改实际上就是对Account中的balance修改。这里主要介绍self的用法,self其实与c++中的this指针有相同的作用。
2、继承
Lua中没有类的概念,对象是没有类型的,但是我们可以为一些对象构建相同的共有的行为,这就是原型,不过我们要模拟类也是可以的。由于原型与类在这里的作用相同,这里仍采用类的叫法。
要在lua中实现原型,可以用继承的方法,这里用到了setmetatable方法。
setmetatable(a, {__index = b})
这样就能让b作为a的原型,当要访问a中的某个字段或函数不存在时,就会在b中查找。
我们再来看一下另一个版本:
localAccountClass =
{
balance = 1000,
New = function(self , obj)
obj = obj or {}
setmetatable(obj , self)
self.__index = self
return obj
end,
Withdraw = function(self , money)
self.balance = self.balance - money
end,
}
localaa = AccountClass:New()
localbb = AccountClass:New()
aa:Withdraw(100)
print(aa.balance) --900
print(bb.balance) --1000
print(AccountClass.balance) --1000
这里多了一个New函数,用于生成以AccountClass为原型的对象。当调用
aa:Withdraw(100)
时,会发生什么?首先lua会在aa中查找Withdraw函数,可以看到New函数中生成的aa只不过是一个空的table,并没有这个函数。于是lua就会查找aa是否具有元表,如果有,就继续在元表中查找该方法:
getmetatable(aa).__index.Withdraw(aa,100)
于是就有
AccountClass.Withdraw(aa,100)
这样就调用了AccountClass的方法,但传入的self对象是aa自己,而不是AccountClass。
注意,这里还有一个很有趣的东西:我们知道aa并没有balance字段,那么如果使用aa.balance会不会修改AccountClass对象的balance字段的值呢?我们把过程分解一下:
aa.balance= aa.balance – money
调用时,会先计算等号右边,lua没有在aa中找到balance字段,就会在其元表中查找,转换为:
aa.balance= getmetatable(aa).__index.balance - money
这其实是一个赋值的过程,这个时候会在aa中生成一个balance字段,值为等号右边的值,这样,aa就继承了AccountClass中的值和方法。
通过上面的实验也可以看出,由AccountClass对象New出来的对象都有自己的字段值和相同的方法,我们称这些对象继承自AccountClass。
3、多态
我们先看下面的例子:
localAccount = {
balance = 1000,
New = function(self , obj)
obj = obj or {}
setmetatable(obj , self)
self.__index = self
return obj
end,
Withdraw = function(self , money)
self.balance = self.balance - money
end,
Deposit = function(self , money)
self.balance = self.balance + money
end,
}
localSpecialAccount = Account:New()
functionSpecialAccount:Withdraw(money)
self.balance = self.balance - money - 5
end
localaa = SpecialAccount:New()
aa:Withdraw(100)
print(aa.balance) --895
aa:Deposit(100)
print(aa.balance) --995
这里,Account定义了两个方法,Withdraw和Deposit,我们用这个对象生成对象SpecialAccount。在lua中对象可以生成对象。SpecialAccount定义了自己的Withdraw方法,用SpecialAccount生成对象aa。当对aa进行操作时,lua首先查找aa是否有需要的内容,没有则会到SpecialAccount中查找(注意,生成aa时setmetatable传入的是SpecialAccount,而不是Account),如果没有再到SpecialAccount的元表Account中查找。
基于lua的这种查找顺序,如果调用Withdraw方法在SpecialAccount中查找到了,就不会再到Account去查找。所以,SpecialAccount定义了自己的Withdraw方法,则其生成的对象就会用这个方法,
由于lua中没有类型,不存在向c++中那样用基类的引用或指针来指向派生类,所以这里只能说由Account生成的使用对象会调用Account的Withdraw方法,而SpecialAccount生成的使用对象会调用SpecialAccount中的Withdraw方法。这里更像是函数的覆盖?