Lua类(面向对象程序设计)

面向对象程序设计

Lua 中的表不仅在某种意义上是一种对象。像对象一样,表也有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值的对象(table)代表两个不同的对象;一个对象在不同的时候也可以有不同的值,但他始终是一个对象;与对象类似,表的生命周期与其由什么创建、在哪创建没有关系。对象有他们的成员函数,表也有:

Account = {balance = 0} 
function Account.withdraw (v) 
 Account.balance = Account.balance - v 
end 
这个定义创建了一个新的函数,并且保存在 Account 对象的 withdraw 域内,下面我们可以这样调用:
Account.withdraw(100.00) 

这种函数就是我们所谓的方法,然而,在一个函数内部使用全局变量名 Account 是一个不好的习惯。

首先,这个函数只能在这个特殊的对象(译者:指 Account)中使用;
第二,即使对这个特殊的对象而言,这个函数也只有在对象被存储在特殊的变量(译者:指 Account)中才可以使用。如果我们改变了这个对象的名字,函数 withdraw 将不能工作:

a = Account; Account = nil
a.withdraw(100.00) -- ERROR! 
这种行为违背了前面的对象应该有独立的生命周期的原则。一个灵活的方法是:定义方法的时候带上一个额外的参数,来表示方法作用的对象。这个参数经常为 self 或者 this:
function Account.withdraw (self, v) 
 self.balance = self.balance - v 
end 
现在,当我们调用这个方法的时候不需要指定他操作的对象了:
a1 = Account; Account = nil
... 
a1.withdraw(a1, 100.00) -- OK 
使用 self 参数定义函数后,我们可以将这个函数用于多个对象上:
a2 = {balance=0, withdraw = Account.withdraw} 
... 
a2.withdraw(a2, 260.00) 

self 参数的使用是很多面向对象语言的要点。大多数 OO 语言将这种机制隐藏起来,这样程序员不必声明这个参数(虽然仍然可以在方法内使用这个参数)。Lua 也提供了通过使用冒号操作符来隐藏这个参数的声明。我们可以重写上面的代码:

function Account:withdraw (v) 
 self.balance = self.balance - v 
end 
调用方法如下:
a:withdraw(100.00) 

冒号的效果相当于在函数定义和函数调用的时候,增加一个额外的隐藏参数。这种方式只是提供了一种方便的语法,实际上并没有什么新的内容。我们可以使用 dot 语法定义函数而用冒号语法调用函数,反之亦然,只要我们正确的处理好额外的参数:

Account = { 
 balance=0, 
 withdraw = function (self, v) 
 self.balance = self.balance - v 
end 
} 
function Account:deposit (v) 
 self.balance = self.balance + v 
end 
Account.deposit(Account, 200.00) 
Account:withdraw(100.00) 

现在我们的对象拥有一个标示符,一个状态和操作这个状态的方法。但他们依然缺少一个 class 系统,继承和隐藏。先解决第一个问题:我们如何才能创建拥有相似行为的多个对象呢?明确地说,我们怎样才能创建多个 accounts?(译者:针对上面的对象Account 而言)

一些面向对象的语言中提供了类的概念,作为创建对象的模板。在这些语言里,对象是类的实例。Lua 不存在类的概念,每个对象定义他自己的行为并拥有自己的形状(shape)。然而,依据基于原型(prototype)的语言比如 Self 和 NewtonScript,在 Lua中仿效类的概念并不难。在这些语言中,对象没有类。相反,每个对象都有一个 prototype(原型),当调用不属于对象的某些操作时,会最先会到 prototype 中查找这些操作。在这类语言中实现类(class)的机制,我们创建一个对象,作为其它对象的原型即可(原型对象为类,其它对象为类的 instance)。类与 prototype 的工作机制相同,都是定义了特定对象的行为。

在 Lua 中,使用前面章节我们介绍过的继承的思想,很容易实现 prototypes.更明确的来说,如果我们有两个对象 a 和 b,我们想让 b 作为 a 的 prototype 只需要:setmetatable(a, {__index = b})

这样,对象 a 调用任何不存在的成员都会到对象 b 中查找。术语上,可以将 b 看作类,a 看作对象。回到前面银行账号的例子上。为了使得新创建的对象拥有和 Account相似的行为,我们使用__index metamethod,使新的对象继承 Account。注意一个小的优化:我们不需要创建一个额外的表作为 account 对象的 metatable;我们可以用 Account表本身作为 metatable:

function Account:new (o) 
 o = o or {} -- create object if user does not provide one 
 setmetatable(o, self) 
 self.__index = self 
return o 
end 

(当我们调用 Account:new 时,self 等于 Account;因此我们可以直接使用 Account取代 self。然而,使用 self 在我们下一节介绍类继承时更合适)。有了这段代码之后,当我们创建一个新的账号并且掉用一个方法的时候,有什么发生呢?
a = Account:new{balance = 0}
a:deposit(100.00)
当我们创建这个新的账号 a 的时候,a 将 Account 作为他的 metatable(调用Account:new 时,self 即 Account)。当我们调用a:deposit(100.00),我们实际上调用的是a.deposit(a,100.00)(冒号仅仅是语法上的便利)。然而,Lua 在表 a 中找不到 deposit,因此他回到 metatable 的__index 对应的表中查找,情况大致如下:

getmetatable(a).__index.deposit(a, 100.00) 
a的metatable是Account,Account.__index也是Account(因为new函数中self.__index 
= self)。所以我们可以重写上面的代码为:
Account.deposit(a, 100.00) 

也就是说,Lua 传递 a 作为 self 参数调用原始的 deposit 函数。所以,新的账号对象从 Account 继承了 deposit 方法。使用同样的机制,可以从 Account 继承所有的域。继承机制不仅对方法有效,对表中所有的域都有效。所以,一个类不仅提供方法,也提供了他的实例的成员的默认值。记住:在我们第一个 Account 定义中,我们提供了成员 balance默认值为 0,所以,如果我们创建一个新的账号而没有提供 balance 的初始值,他将继承默认值:

b = Account:new() 
print(b.balance) --> 0 
当我们调用 b 的 deposit 方法时,实际等价于:
b.balance = b.balance + v 

(因为 self 就是 b)。表达式 b.balance 等于 0 并且初始的存款(b.balance)被赋予b.balance。下一次我们访问这个值的时候,不会在涉及到 index metamethod,因为 b 已经存在他自己的 balance 域

发布了290 篇原创文章 · 获赞 153 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39885372/article/details/104404428