Lua中的table就是一种对象。首先,table与对象一样可以拥有状态;其次,table与对象一样拥有一个
独立于其值的标识(一个self)。最后,table与对象一样具有独立于创建者的生命周期。
在函数中使用全局名称是一个不好的编程习惯。因为这个函数只能针对特定对象工作,并且这个特定
对象还必须存储在特定的全局变量中。这种行为违反了对象特性,即对象拥有独立的声明周期。因此
需要一个额外的参数来指定一项操作所作用的接受者。这个参数通常称为self或this。当调用这个方法时,
必须指定其作用的对象。
Account = {balance = 0}
function Account.withdraw(self, v)
self.balance= self.balance - v
end
a1 = Account; Account = nil
a1.withdraw(a1, 100.00)
Lua中使用冒号可以隐藏self参数。
function Account:withdraw(v)
self.balance= self.balance - v
end
调用时可写为 a1:withdraw(100.00)
冒号的作用是在一个方法定义中添加一个额外的隐藏参数,以及在一个方法调用中添加一个额外的实参。
冒号只是一种语法便利,并没有引入任何新的东西。使用时只要能正确地处理那个额外参数即可。
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)系统、继承和私密性(privacy)。
16.1 类
一个类就是一个创建对象的模具。Lua中没有类的概念,但是可以通过原型来模拟类。类和原型都是一种组织
对象间共享行为的方式。
在Lua中可以通过13.4.1节中所描述的继承实现原型。准确地说,如果有两个对象a和b,要让b作为a的一个原型,
只需要输入如下语句:
setmetatable(a, {__index = b})
在此之后,a就会在b中查找所有它没有的操作。可以称b为时对象a的类。
之前银行账号的示例。为了创建更多与Account行为类似的账号,可以让这些新对象从Account行为中继承这些操作。
function Account:new(o)
o = o or {} -- 如果用户没有提供table,则创建一个
setmetatable(o,self) -- 使用Account table自身作为元表
self.__index= self
return 0
end
在这段代码之后,创建一个新账户或调用一个方法
a = Account:new{balance = 0}
a:deposit(100.00)
最终的调用情况为:
getmetatable(a).__index.deposit(a,100.00)
a的元表是Account,Account.__index也是Account。因此,上述表达式可以简化为
Account.deposit(a, 100.00)
新账户a从Account继承了deposit函数,同样它还能从Account继承所有的字段。
16.2 继承
假设有一个基类Account:
Account = {balance = 0}
function Account:new(o)
o = o or {}
setmetatable(o,self)
self.__index =self
return o
end
function Account:deposit(v)
self.balance= self.balance + v
end
function Account:withdraw(v)
if v >self.balance then error("insufficient funds") end
self.balance= self.balance - v
end
若想从这个类派生出一个子类SpecialAccount,以使客户能够透支,则先需要创建一个空类,
从基类继承所有的操作:
SpecialAccount = Account:new()
现在SpecialAccount可以重定义那些从基类继承的方法:
s = SpecialAccount:new{limit=1000.00}
function SpecialAccount:withdraw(v)
if v -self.balance > self:getLimit() then
error "insufficient funds"
end
self.balance= self.balance - v
end
function SpecialAccount:getLimit()
returnself.limit or 0
end
现在调用s:withdraw(200.00)时,Lua就不会在Account中查找了。因为Lua会在SpecialAccount中先找到withdraw方法。
如果只有一个对象需要某种特殊的行为,那么可以直接在该对象中实现这个行为。
16.3 多重继承
在Lua中使用__index元方法实现面向对象编程是一种集建议、性能和灵活性于一体的做法;
如果要实现多重继承,关键在于用一个函数作为__index元字段。多重继承意味着一个类可以具有多个基类。
因此无法使用一个类中的方法来创建子类,而是需要一个特殊的函数来创建——createClass,它创建一个table
表示新类,其中一个参数表示新类的所有基类。创建时,它会设置元表中__index元方法,多重继承正是在这个
__index元方法中完成的。
local function search(k, plist) -- 在table plist中查找k
for i=1,#plist do
local v =plist[i][k]
if v thenreturn v end
end
end
function createClass(...)
local c = {} --创建新类
local parents = {...} -- 类在其父类列表中的搜索方法
setmetatable(c,{__index= function(t, k)
return search(k, parents)
end})
c.__index = c -- 将'c'作为其实例的元表
functionc:new(o) -- 为这个新类定义一个新的构造函数
o = o or{}
setmetatable(o,c)
return o
end
return c -- 返回新类
end
使用createClass的例子。假设有两个类,一个是前面提到的Account类;另一个是Named类,它有两个
方法setname 和 getname。
Named = {}
function Named:getname()
return self.name
end
function Named:setname()
self.name = n
end
要创建一个新类NamedAccount,同时从Account和Named派生,只需调用createClass:
NamedAccount = createClass(Account, Named)如下要创建并使用实例:
account = NamedAccount:new{name = "Paul"}
print(account:getname()) --> Paul
因为需要搜索字段,多重继承的性能不如单一继承。一种改进性能的做法是将继承的方法复制到子类中。
通过这种技术,类的__index元方法如下所示:
setmetatable(c, {__index = function(t, k)
local v = search(k, parents)
t[k] = v -- 保存下来,以备下次访问
return v
end})
16.4 私密性(privacy)
Lua在设计对象时,没有提供私密性机制。但可以用其它方法实现对象的访问控制;
这种实现不常用,只做基本的了解即可。这种做法的基本思想是通过两个table来
表示一个对象,一个table用来保存对象的状态;另一个用于对象的操作。对象本身
通过第二个table来访问,即通过其接口的方法来访问。
示例——使用这种设计来表示一个银行账户:
function newAccount(initialBalance)
local self ={balance = initialBalance}
local withdraw = function(v)
self.balance = self.balance - v
end
local deposit= function(v)
self.balance= self.balance + v
end
local getBlance = function()
return self.balance
end
return {
withdraw = withdraw,
deposit =deposit,
getBalance= getBalance
}
end
这种设计给予存储在self table中所有东西完全的私密性。当newAccount返回后,就无法直接访问这个table了。
只能通过newAccount中创建的函数来访问它。
16.5 单一方法(single-method)做法
面向对象编程的做法有一种特殊情况,就是当一个对象只有一个方法时,可以不用创建接口table,但要将
这个单独的方法作为对象表示来返回。参阅7.1节;
单一方法对象还有一种情况,若这个方法是一个调度(dispatch)方法,它根据某个参数来完成不同的操作。
如想可以这样来实现一个对象:
function newObject(value)
return function (action, v)
if action == "get" then returnvalue
elseif action == "set" then
value = v
else
error("invalidaction")
end
end
end
如下所示:
d = newObject(0)
print(d("get")) --> 0
d("set", 10)
print(d("get")) --> 10
这种非传统的对象实现方式是很高效的。虽然无法实现继承,却拥有了完全的私密性控制。