Chapter 16: Object-Oriented Programming
表是对象
表可以有自已的成员函数:
Account = {balance = 0}
function Account.withdraw (v)
Account.balance = Account.balance - v
end
Account.withdraw(100.00)
但是在函数内部使用全局名Account 是不好的编程实践。
更灵活的方法:
Account = {balance = 0}
function Account.withdraw (self, v)
self.balance = self.balance - v
end
a1 = Account
a1.withdraw(a1, 100.00)
-- 创建对象的方法有点麻烦,不过成员函数得以复用了
a2 = {balance=0, withdraw = Account.withdraw}
a2.withdraw(a2, 260.00)
self 参数的使用是面向对象语言的关键。Lua 中使用colon operator 隐藏self。
使用colon operator 代替显示的写self
Account = {balance = 0}
function Account:withdraw (v)
self.balance = self.balance - v
end
a1 = Account
a1:withdraw(100.00)
a2 = {balance=0, withdraw = Account.withdraw}
a2:withdraw(100.00)
我们可以以dot syntax 定义函数,然后以colon syntax 语法调用它。也可以以colon syntax 语法定义,以dot syntax 语法调用,只要正确的处理额外的self 参数。
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)
我们的对象有标识,状态,成员函数。但是还缺类系统,继承和私有机制。
16.1 Classes
类作为创建对象的模型。对象是类的实例。Lua 没有类的概念,但是可以模拟。
使用继承可以轻易实现原型。如果我们有a,b 两个对象,所有我们要做的就是:
setmetatable(a, {__index = b})
new 一个对象
Account = {
balance=0,
withdraw = function (self, v)
self.balance = self.balance - v
end
}
function Account:new (o)
o = o or {} -- create table if user does not provide one
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit (v)
self.balance = self.balance + v
end
Account.deposit(Account, 200.00)
Account:withdraw(100.00)
a = Account:new{balance = 0}
a:deposit(100.00)
lua 在表a 中找不到“deposit” 项,所以在a的元表中找。
a:deposit(100.00) 大概等价于:
getmetatable(a).__index.deposit(a, 100.00)
a 的元表是Account,而且Account.__index 也是Account(因为self.__index = self),所以前面的代码等价于:
Account.deposit(a, 100.00)
就是说,Lua 调用原始的deposit 函数,但是传递a 作为self 参数。所以新对象a 从对象Account 那里继承了deposit 函数。通过同样的机制,它可以继承Account 的所有东西。
不只是函数,一个对象还可以继承另一个对象的成员:
b = Account:new() -- b.blance 是对Account.balance 的引用
print(b.balance) --> 0
b 的balance 如果不存在就会去元表里找,结果就引用了Account.balance
对deposit 方法的调用等价于:
b.balance = b.balance + v
右边的b.balance 是对Account.balance 的引用,整个语句执行完后b 就拥有自已的blance 域了。
16.2 Inheritance
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 = Account:new()
s = SpecialAccount:new{limit=1000.00}
s:deposit(100.00)
Lua 在s中找不到deposit 域,所以到SpecialAccount 中找;还是找不到,所以又去Account 中找。
SpecialAccount 特别之处在于我们可以重定义任意从基类继承来的函数。所要做的只是写一个同名的新方法:
函数重载
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 = Account:new()
function SpecialAccount:withdraw (v)
if v - self.balance >= self:getLimit() then
error"insufficient funds"
end
self.balance = self.balance - v
end
function SpecialAccount:getLimit ()
return self.limit or 0
end
s = SpecialAccount:new{limit=1000.00}
s:withdraw(200.00)
现在,当我们调用s:wthdraw(200.00),lua 不会进入Account,因为它首先在SpecialAccount中找到了新的withdraw 方法。
还可以再次在s 中重载:
function s:getLimit ()
return self.balance * 0.10
end
16.3 Multiple Inheritance
因为对象不是lua 的原生类型(模似的),有许多方法在lua 中实现面象对向编程。我们已经看到过的,使用index 元方法是最好的简洁,高效,灵活的方法。虽然如此,还有其它一些方法,它们也许更适合一些特殊情况。下面,我们来看看lua 中如何实现多继承。
一个类不能同时做为其实例和其子类的元表。
多继承
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
Named = {}
function Named:getname ()
return self.name
end
function Named:setname (n)
self.name = n
end
-- look up for 'k' in list of tables 'plist'
local function search (k, plist)
for i=1, #plist do
local v = plist[i][k] -- try 'i'-th superclass
if v then return v end
end
end
function createClass (...)
local c = {} -- NamedAccount 就是c 的引用
local parents = {...}
-- class will search for each method in the list of its parents
setmetatable(c, {__index = function (t, k) -- 设置NamedAccount 的元表,元表有一个__index 函数,NamedAccount 中没有的东西就在__index 中找,既是在两个基类中找
return search(k, parents)
end})
c.__index = c
function c:new (o) -- NamedAccount 的new
o = o or {}
setmetatable(o, c)
return o
end
return c -- return new class
end
NamedAccount = createClass(Account, Named)
account = NamedAccount:new{name = "Paul"} -- NamedAccount 的new 将表{name = "Paul"} 的元表设置为NamedAccount
print(account:getname()) --> Paul
NamedAccount 同时是Account 和Named 的子类。
当然,多继因为复杂的搜索会有性能上的损失。
一个简单的改善多继承方法是拷贝继承方法到子类:
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
Named = {}
function Named:getname ()
return self.name
end
function Named:setname (n)
self.name = n
end
-- look up for 'k' in list of tables 'plist'
local function search (k, plist)
for i=1, #plist do
local v = plist[i][k] -- try 'i'-th superclass
if v then return v end
end
end
function createClass (...)
local c = {} -- NamedAccount 就是c 的引用
local parents = {...}
-- class will search for each method in the list of its parents
setmetatable(c, {__index = function (t, k) -- 设置NamedAccount 的元表,元表有一个__index 函数,NamedAccount 中没有的东西就在__index 中找,既是在两个基类中找
local v = search(k, parents)
t[k] = v -- 将基类的方法拷贝到本地 -- save for next access
return v
end})
c.__index = c
function c:new (o) -- NamedAccount 的new
o = o or {}
setmetatable(o, c)
return o
end
return c -- return new class
end
NamedAccount = createClass(Account, Named)
account = NamedAccount:new{name = "Paul"} -- NamedAccount 的new 将表{name = "Paul"} 的元表设置为NamedAccount
print(account:getname()) --> Paul
使用这种技巧,访问继承方法和本地方法一样快(除了第一次访问)。
16.4 Privacy
Lua 不支持私有机制,部分原因是因为使用表来表示对象。但是,这也反映了lua 背后的基础设计决策,设计大规模应用程序并不是Lua 的初衷。
虽然Lua 不支持私有机制,但是对象可以有不同的实现,所以我们可以对访问进行控制。虽然这种实现不常使用,而了解它是有益的。有两个原因,一是因为它展示了Lua 有趣的“犄角旮旯”,二是因为它可以成为一个在解决其它问题时很好的方案。
这种设计的基本想法是将每一个对象用两个表来表示:一个作为它的状态,另一个作为它的操作或称为接口。对象本身通过第二个表访问。为避免未授权的访问,对象中表示状态的那个表不是其它表的一个域,而是一个特定方法里面的一个闭包。
用闭包实现对象
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 getBalance = function () return self.balance end
return {
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
acc1 = newAccount(100.00)
acc1.withdraw(40.00)
print(acc1.getBalance()) --> 60
首先,函数创建一个用来存内部对象状态的表,并把它保存在一个局部变量self 中。然后,函数创建对象的方法。最后,函数创建并返回一个包含了方法引用的表。这里的关键是这些方法不需要一个self 作为额外参数,而是直接访问self 本身。因为不再有外部参数,我们就不需要使用colon syntax 语法来操作对象了。方法的调用就像普通函数一样:
这个方案使得存在self 表里面的任何东西都变成私有的了。当newAccount 返回后没有方法可以访问这个表,只能通过由newAccount 创建的方法访问。虽然这个例子只在私有表中放了一个实例变量,但我们可以将所有私有部分都放入这个表。我们也可以定义私有方法:它们和公有方法差不多,但是不放入接口中。
私有方法
function newAccount (initialBalance)
local self = {
balance = initialBalance,
LIM = 10000.00,
}
local extra = function () -- 私有方法
if self.balance > self.LIM then
return self.balance*0.10
else
return 0
end
end
local withdraw = function (v)
self.balance = self.balance - v
end
local deposit = function (v)
self.balance = self.balance + v
end
local getBalance = function ()
return self.balance + extra() -- 调用私有方法
end
return {
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
acc1 = newAccount(100.00)
acc1.withdraw(40.00)
print(acc1.getBalance()) --> 60
同样,这儿没有可供用户直接访问私有函数extra 的方法。
16.5 The Single-Method Approach
前面例子的一种特殊情况是,一个对象只有一个方法。这种时侯,我们就不需要去创建一个接口表了。我们返回这个方法作为对象表示。如果这听起来让人有点困惑,回想一下7.1 节是有益的。那里我们看到,我们如何创建一个用闭包来保存状态的迭代器。
单方法对象
function newAccount (initialBalance)
local self = {
balance = initialBalance,
LIM = 10000.00,
}
local extra = function () -- 私有方法
if self.balance > self.LIM then
return self.balance*0.10 -- 超过10000 有%10 的加成效果
else
return 0
end
end
return function ()
return self.balance + extra() -- 调用私有方法
end
end
acc1 = newAccount(11000.00) -- 12100
print(acc1()) -- 对象的用法像C++ 的仿函数
另一个有趣的情况是单方法对象的方法实际上是一个分派方法,它基于不同的参数而执行不同的任务。这种对象的可能实现像这样:
基于不同参数而执行不同的任务的对象
function newObject (value)
return function (action, v)
if action == "get" then
return value
elseif action == "set" then
value = v
else
error("invalid action")
end
end
end
d = newObject(0) -- 对象d 其实就是一个闭包
print(d("get")) --> 0
d("set", 10)
print(d("get")) --> 10
这种有别于传统的对象实现效率非常高。每一个对象使用单独的闭包,比一个表的开销要小。这里没有继承,但有完整的私有机制:仅有一种方法访问对象的状态,就是通过那个单独的方法。