截止至目前,我们的对象具有了标识,状态和对状态进行的操作,但还缺乏类体系、继承和私有性。让我们先来解决第一个问题,即应该如何创建多个具有类似行为的对象。更具体地说,我们应该如何创建过个银行账户呢?
大多数面向对象语言提供了类的概念,类在对象的创建中扮演了模板的作用。在这些语言中,每个对象都是某个特定类的实例。 Lua
语言中没有类的概念,虽然元表的概念在某种程度上与类的概念相似,但是把元表当做类使用在后续会比较麻烦。相反,我们可以参考基于原型的语言中的一些做法来在 Lua
语言中模拟类,例如 JavaScript
语言。在这些语言中,对象不属于类。相反,每个对象可以有一个原型( prototype
)。原型也是一种普通的对象,当对象(类的实例)遇到一个未知操作时会首先在原型中查找。要在这种语言中表示一个类,我们只需要创建一个专门被用作其他对象(类的实例)的原型对象即可。类和原型都是一种组织多个对象间共享行为的方式。
在 Lua
语言中,我们可以使用__index 元方法中所述的继承的思想来实现原型。更准确地说,如果有两个对象 A
和 B
,要让 B
成为 A
的一个原型,只需要:
setmetatable(A,{__index = B})
在此之后, A
就会在 B
中查找所有它没有的操作。如果把 B
看作对象 A
的类,则只不过是术语上的一个变化。
让我们回到之前银行账号的示例,为了创建其他与 Account
行为类似的账号,我们可以使用 __index
元方法让这些新对象从 Account
中继承这些操作。
local mt = { __index = Account } function Account.new(o) o = o or {} setmetatable(o, mt) return o end
在这段代码执行后,当我们创建一个新账户并调用新账户的一个方法时会发生什么呢?
a = Account.new{balance=0} a:deposit(100.00)
当我们创建一个新账户 a
时, a
会将 mt
作为其元表。当调用 a:deposit(100.00)
时,实际上调用的是 a.deposit(a, 100.00)
,冒号只不过是一个语法糖。不过, Lua
语言无法在表 a
中找到字段 "deposit"
,所以他会在元表的 __index
中搜索。此时的情况大致如下:
getmetatable(a).__index.deposit(a, 100.00)
a
的元表是 mt
,而 mt.__index
是 Accoun
t。因此,上述表达式等价于:
Account.deposit(a, 100.00)
即, Lua
语言调用了原来的 deposit
函数,传入了 a
作为 self
参数。因此,新账户 a
从 Account
继承了函数 deposit
。同样,它还从 Account
继承了所有的字段。
对于这种模式,我们可以进行以下两个小改进:
- 不创建扮演元表角色的新表而是把表
Account
直接用作元表 - 对
new
方法也使用冒号语法
加入上述两个改进之后,方法 new
会变成:
function Account:new (o) o = o or {} self.__index = self setmetatable(o, self) return o end
现在,当我们调用 Account.new()
时,隐藏的参数 self
得到的实参是 Account
, Account.__index
等于 Account
,并且 Account
被用作新对象的元表。可能看上去第二种修改(冒号语法)并没有得到大大的好处,但实际上当我们引入类的继承时,使用 self
的优点就会很明显了。
继承不仅可以用作方法,还可以用作于其他在新账户中没有的字段。因此,一个类不仅可以提供方法,还可以为实例中的字段提供常量和默认值。请注意,在第一版的 Account
的定义中,有一个 balance
字段的值是 0
。因此,如果在创建新账户时没有提供初始的余额,那么余额就会继承这个默认值:
b = Account:new() print(b.balance) --> 0
当在 b
上调用 deposit
方法时,由于 self
就是 b
,所以等价于:
b.balance = b.balance + v
表达式 b.balance
求值后等于零,且该方法给 b.balance
赋了初始的金额。由于此时b有了它自己的 balance
字段,因此后续对 b.balance
的访问就不会再涉及元方法了。