第十六课 面向对象编程

来源:互联网 发布:编程之魂 闫怀志 编辑:程序博客网 时间:2024/06/05 19:10
Lua中的table就是一种对象,这句话从3个方面来证实:
1、table与对象一样可以拥有状态。
2、table也与对象一样拥有一个独立于其值的 标识(一个self)。例如,两个具有相同值的对象(table)是两个不同的对象。
3、table与对象一样具有独立于创建者和创建地的生命周期。
table也有和对象一样的操作:
Account = {balance = 0}
function Account.withdraw (v)
Account.balance = Account.balance - v
end
可以这样调用:
Account.withdraw(100.00)
这种函数就是所谓的“方法(Method)”。不过,在函数中使用全局名称Account是一个不好的编程习惯。因为这个函数只能针对特定对象工作,并且,这个特定对象还必须存储在特定的全局变量中。如果改变了对象的名称,withdraw就再也不能工作了:
a = Account; Account = nil
a.withdraw(100.00) --错误!
这种行为违反了前面提到的对象特性,即对象拥有独立的生命周期。
有一种灵活的方法,即指定一项操作所作用的“接受者”。因此需要 一个额外的参数来表示该接受者。这个参数通常称为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, 200.00)
使用self参数是所有面向对象语言的一个核心。大多数面向对象语言都能对程序员隐藏部分self参数,从而使得程序员不必显示地声明这个参数。Lua只需要使用冒号,则能隐藏该参数。即可将上例重写:
function Account:withdraw (v)
self.balance = self.balance - v
end
调用时可写为:
a: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(200.00)
现在的对象table已经有一个标识、一个状态和状态之上的操作。不过还缺乏一个类(class)系统、继承和私密性(privacy)。首先,解决如何创建多个具有类似行为的对象?更准确地说,如何创建多个Account账户对象。

一个类就像是创建对象的模具。有些面向对象语言提供了类的概念,在这些语言中每个对象都是某个特定类的实例。Lua则没有类的概念,每个对象只能自定义行为和形态。不过,要在Lua中模拟类也并不困难,可以 参照一些基于原型的语言,例如Self和 NewtonScript。在这些语言中,对象是没有“类型”的(objects have no classes)。而是每个对象都有一个原型(prototype)。原型也是一种常规的对象,当其他对象(类的实例)遇到一个未知操作时,原型先会查找它。在这种语言中要表示一个类,只需要创建一个专用作其他对象(类的实例)的原型。类和原型都是一种组织对象间共享行为的方式。
在Lua中实现原型很简单,如果两个对象a和b,要让b作为a的一个原型,只需要输入如下语句:
setmetatable(a, {__index = b})
在此之后,a就会在b中查找所有它没有的操作。若将b称为是对象a的类,只不过是术语上的变化。
为了创建更多与Account行为类似的账号,可以让这些新对象从Account行为中继承这些操作。具体的做法就是使用__index元方法。可以应用 一项小优化,则 无须创建一个额外的table作为 账户对象的元表。而是使用Account table自身作为元表:
function Account:new (o)
o = o or {}
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)
继承不仅可以作用于方法,还可以作用于所有其他在新账户中没有的字段。因此,一个类不仅可以提供方法,还可以为实例中 的字段提供默认值。
b = Account:new()
print(b.balance) -->0
b:deposit(100.00)
在b上调用deposit方法时,self就是b,相当于执行了:
b.balance = b.balance + v
在第一次调用deposit时,对表达式b.balance的求值结果为0,然后一个初值被赋予了b.balance。后续对b.balance的访问就不会再涉及到__index元方法了。因为此时b已有自己的balance字段。

继承
由于类也是对象,它们也可以从其他类获得方法。这种行为就是一种继承。
假设有一个基类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()
s = SpecialAccount:new{limit = 1000.00}
SpecialAccount之所以特殊是因为可以重定义那些从基类继承的方法。编写一个方法的新实现只需:
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:withdraw(200.00)时,Lua就不会在Account中查找了。因为Lua会在SpecialAccount中先找到withdraw方法。
Lua中的对象有一个特殊现象,就是无须为指定一种新行为而创建一个新类。如果只有一个对象需要某种特殊的行为,那么可以直接在该对象中实现这个行为。例如,账户s表示一个特殊的客户,这个客户的透支额度总是其余额的10%。那么可以只修改这个对象:
function s:getLimit ()
return self.balance * 0.10
end
在这段代码后,调用s:withdraw(200.00)还是会执行SpecialAccount的withdraw。但withdraw所调用的self:getLimit则是上面这个定义。

多重继承
由于Lua中的对象不是原生的,因此在Lua中进行面向对象编程时有几种方法。上面介绍了一种使用__index元方法的做法,这是最简易、性能和灵活性于一体的做法。另外还有一些其他的做法,可能更适用于某些特殊的情况。在此将介绍另一种做法,可以在Lua中实现多重继承。
这种做法的关键在于用一个函数作为__index的元字段。例如,若在一个table的元表中,__index字段为一个函数,那么只要Lua在原来的table中找不到一个key,就会调用这个函数。基于这点,就可以让__index函数在其他地方查找缺失的key。
多重继承意味着一个类可以具有多个基类。因此无法使用一个类中的方法来创建子类,而是需要定义一个特殊的函数来创建。下面的createClass就是这样的函数,它会创建一个table表示新类,其中一个参数表示新类的所有基类。创建时它会设置元表中的__index元方法,而多重继承正是在这个__index元方法中完成的。虽然是多重继承,但每个对象实例仍属于单个类,并且都在这个类中查找所有的方法。因此,类和基类之间的关系 不同于类和实例之间的关系。尤其是一个类不能同时作为其实例和子类的元表。在以下代码中,将类作为其实例的元表,并创建了另一个table作为类的元表。
--在table plist中查找k
local function search (k, plist)
for i, #plist do
local v = plist[i][k]
if v then return v end
end
end

function createClass (...)
local c = {} --新类
local parents = {...}
--类在其父类列表中的搜索方法
setmetatable(c, {__index = function (t, k)
return search(k, parents)
end})
--将c作为其实例的元表
c.__index = c
--为这个新类定义一个新的构造函数
function c:new (o)
o = o or {}
setmetatable(o, c)
return o
end
return c --返回新类c
end
假设有两个类,一个是前面的Account类;另一个是Named类,它有两个方法setname和getname:
Named = {}
function Named:setname (n)
self.name = n
end
function Named:getname ()
return self.name
end
要创建一个新类 NamedAccount,同时从Account和Named派生,那么只需要调用createClass:
NamedAccount = createClass(Account, Named)
如下要创建并使用实例:
account = NamedAccount:new{name = "Paul"}
print(account:getname()) -->Paul
首先,Lua在account中无法找到字段“getname”。因此,就查找account元表中的__index字段,该字段为NamedAccount。由于在NamedAccount中也无法提供字段“getname”,因此,Lua查找NamedAccount元表中的__index字段。由于这个字段也是一个函数,Lua就调用了它。该函数则现在Account中查找“getname”,未找到后,继而查找Named。最终在Named中找到了一个非nil的值,即为搜索的最终结果。
由于这项搜索具有一定的复杂性,则多重继承的性能不如单一继承。有一种改进性能的简单做法是将继承的方法复制到子类中。通过这种技术,类的__index元方法如下所示:
setmetatable(c, {__index = function (t, k)
local v = search(k, parents)
t[k] = v --保存下来,以备下次访问
return v
end})
用了这种技术后,访问继承的方法就能像访问局部方法一样快了。但缺点是当系统运行后就较难修改方法的定义,因为这些修改不会沿着继承体系向下传播。

私密性
Lua在设计对象时,没有提供私密性机制。一方面因为使用了普通的结构table来表示对象,另一方面也反映了Lua某些基本的设计决定。Lua并不打算构建需要许多程序员长期投入的大型程序。相反,Lua定位于开发中小型程序,这些程序通常是一个更大系统的一部分。Lua尽量避免过多冗余和人为限制。如果不想访问一个对象中的内容,则无须进行操作。
Lua的 另外一项设计目标是灵活性。Lua提供给程序员各种元机制,以使他们能模拟许多不同的机制。虽然在Lua对象的基础设计中没有提供私密性机制,但可以用其他方法来实现对象,从而获得对象的访问控制。这种实现不常用,只做基本了解,它 既探索了Lua中的某些知识又可以成为其他问题的解决方案。
这种做法的基本思想是,通过两个table来表示一个对象。一个table用来保存对象的状态;另一个用于对象的操作,或称为“接口”。对象本身是通过第二个table来访问的,即通过接口的方法来访问。为了避免未授权的访问,表示状态的table不保存在其他table中,而只是保存在方法的closure中。例如,若使用这种设计来表示一个银行账户,可以调用下面这个工厂函数来创建新的账户对象:
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
这个函数先创建了一个table,用于保存对象的内部状态,并将其 存储在局部变量self中。然后再创建对象的方法。最后,函数创建并返回一个供外部使用的对象,其中将方法名与真正的方法实现匹配起来。区别关键在于,这些方法不需要额外的self参数,因为它们可以直接访问self变量。由于没有了额外的参数,也就无须使用冒号语法来操作对象。则可以像普通函数那样来调用这些方法:
acc1 = newAccount(100.00)
acc1.withdraw(40.00)
print(acc1.getBalance()) -->60
这种设计给予存储在self table中所有东西完全的私密性。当newAccount返回后,就无法直接访问这个table了。只能通过newAccount中创建的函数来访问它。上例只将一个成员变量放到了私有table中,其实可以将一个对象中所有的私有部分都存入这个table。另外还可以定义私有的方法,它们类似于 公有方法,但不放入接口中。
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 getBalance = function ()
return self.balance + extra()
end
<如前>
end
与前一个示例一样,任何用户都无法直接访问extra函数。

单一方法(single method)做法
上述面向对象编程的做法有一种特殊情况,就是当一个对象只有一个方法时,可以不用创建接口table,但要将这个单独的方法作为对象表示来返回。一个具有状态的迭代器是一个单一方法对象。
function values (t)
local i = 0
return function () i = i + 1; return t[i] end
end
单一方法对象还要一种情况,若这个方法是一个调度(dispatch)方法,它根据某个参数来完成不同的操作。则可以这样实现一个对象:
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)
print(d("get")) -->0
d("set", 10)
print(d("get")) -->10
这种非传统的对象实现方式是很高效的。语句d("set", 10)虽然有些 奇特, 但只比传统的d:set(10)多出两个字符。每个对象都用一个closure,这比都用一个table更高效。虽然无法实现继承,却拥有了完全的私密性控制。访问一个对象状态只有一个方式,那就是通过它的单一方法。



0 0
原创粉丝点击