《Programming in Lua 3》读书笔记(十二)
日期:2014.7.14
PartⅡ Object-Oriented Programming
Lua中实现面向对象编程。
“如同OOP对象,table拥有状态;如同OOP对象,table拥有标识符---self,用来与其他变量做区分,而且两个table拥有同样的值也是不同的object(对象),因为self的不同;如同OOP对象,table也有生命周期,这个生命周期与谁在何处创建table是保持独立的”
对象是拥有自己的运算操作的,table也有,如
e.g. Account = {blanche = 0} function Account.withdraw(v) Account.balance = Account.balance - v end
上述的函数就是OOP中称呼的method(方法)。当然,上述的使用技巧是不可取的:在函数体内使用全局变量Account。这样会造成严重的后果,而且这样使用限制性太大,当我们改变了变量类型,这个操作就失效了。这种操作与面向对象编程中对象保持独立的生存周期这一原则相悖。
e.g. a , Account = Account,nil a.withdraw(100.00) --error
因为我们将Account赋值为nil了,所以withdraw函数就会报错。
针对上述的操作改进:我们可以传递额外的参数,作为函数运算的对象
e.g. function Account.withdraw(self,v) self.balance = self.balance - v end
此时
a , Account = Account,nil a.withdraw(100.00) --ok
但是在大多数面向对象编程的语言中,一般都是隐藏我们上述用到的那个参数。Lua也能隐藏这个参数,这里就要使用到冒号操作符:
e.g. function Account:withdraw(v) self.balance = self.balance - v end
当然,这里使用冒号操作符只是一个语法约定而已,没有额外的意思。我们可以用点号运算符定义一个函数然后用冒号运算符调用该函数,反之亦然
e.g. 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)
我个人还是觉得按套路来,遵循这种语法约定。
16.1 Classes
Lua中没有类(class)的概念,但是很容易模仿出类。参考了prototype-base language(面向原型编程)中prototype(原型)的相关技巧。在这种语言中,也是没有类,但是每个对象都拥有一个原型。在这种语言环境下要表现出类的概念,我们只需要为继承者创建一个唯一的对象作为原型。类和原型的目的都在于共享某些行为。
前面在讨论元表的时候有提到继承,因此假如现在有两个对象a和b,采用如下操作便可将b设置为a的原型:
e.g. setmetatable(a,{__index = b})
执行了这个操作之后,假如我们访问a中的成员,在找不到的时候会访问b。
回到现在讨论的类,假如我们需要创建一个新的account,其行为与Account一样,在这里我们就可以考虑使用继承,使用 __index 元方法。在这里我们不需要额外创建一个新的table作为元表,可以直接将我们要继承的table设置为其元表:
e.g. function Account:new(o) o = o or {} setmetatable(o,self) self.__index = self return o end
这里使用到了前文提到的冒号操作符,默认使用了self参数。
此时
a = Account:new(balance = 0} a:deposit(100.00)
我们新建了一个table a,其元表为Account,又修改了其元方法__index 为Account 自身,当我们在a中寻找deposit的时候,找不到的时候会自动在Account中寻找,达到了继承的要求。
创建a的时候,将balance赋值为了0,假如不给其赋值,则会继承其默认值
b = Account:new() print(b.balance) --- 0 继承了Account的balance的值0
16.2 Inheritance
继承
Lua中实现继承还是比较容易的
e.g. --基类 Account = {balance = 0 } function Account:new(o) o = o or {} setmetatable(0,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 "xxx" end self.balance = self.balance - v end
现在我们想写一个子类继承这个基类,然后能在子类中做进一步的修改,可以这样操作
SpecialAccount = Account:new()
执行以上操作之后,SpecialAccount 便是Account的一个实例了(--modify 应该是继承而非实例吧?),当我们执行一下操作:
s = SpecialAccount:new(limit = 1000.00}
SpecialAccount 从基类中继承了new这个方法,因为这里使用了冒号操作符,默认使用了SpecialAccount这个参数,因此此时s的元表是SpecialAccount。当我们试图访问s中不存在的元素的时候,便会去SpecialAccount中寻找,而从SpecialAccount中寻找不到的时,转而会去Account中寻找。
e.g. s:deposit(100.00)
此时lua会在s、SpecialAccount、Account里面寻找deposit方法
我们可以在子类中重新定义从基类中继承的方法:
e.g. function SpecialAccount:withdraw(v) if v - self.balance >= self.getLimit() then error"xx" end self.balance = self.balance - v end function SpecialAccount:getLimit() return self.limit or 0 end
此时,当我们调用s:withdraw的时候,lua会直接在SpecialAccount找到该方法,执行该方法内的操作。
而lua中有趣的一点是,不需要重新创建一个新的类来实现一个新的行为,可以直接在对象中实现该行为,如:
上文我们已经创建了SpecialAccount对象s,我们要在s中实现一个限制行为,限制每次的操作限额,我们可以这样实现:
e.g. function s:getLimit() return self.balance * 0.10 end
这样,当我们调用s:withdraw的时候,条件判断getLimit会直接得到s已经定义的行为,而不会再去SpecialAccount中寻找。
16.3 Multiple Inheritance
多重继承
Lua中实现面向对象编程是有很多种途径的,上文中提到的使用 __index 元方法是一种便捷的方式。在不同的情况下需要选择不同的实现方式,在这里介绍的是一种能实现多重继承的方法。
这里也涉及到了使用__index 元方法,在该方法内使用一个函数。当table的元表的 __index 字段中有一个函数的时候,Lua都会调用该函数而不管有没有在该table中寻找到key。
多重继承的思想在于一个类可以有多个父类。因此我们就不能用类的方法来创建子类,而是定义一个函数来实现该功能--createClass,以父类作为参数来创建子类。这个函数创建一个table来代表新的类,然后设置元表的元方法__index 来实现多重继承。在这里有要注意的地方,类和父类的关系与类和实例的关系是有差异的,一个类不能同时成为其实例和其子类的元表。
e.g. --假定现在有两个类,之前的Account和现在的Named Named = {} function Named:getname() return self.name end function Named:setname(v) self.name = v end --在plist这个table中寻找k local function search(k,plist) for i = 1,#plist do local v = plist[i][k] if v then return v end end end function createClass(…) local c = {} --新的类 local parents = { … } --从父类table中找到各个父类中的方法 setmetatable(c,{ __index = function (t,k) return search(k,parents) end} ) --多重继承的技巧在于此处,__index 元方法是一个函数,该函数会从父类列表中寻找每个父类中的所有方法,这样就实现了多重继承 --新的类成为其实例的元表 c.__index = c --创建新的类的构造方法 function c:new(o) o = o or {} setmetatable(o,c) return o end return c end
现在我们就能创建一个多重继承的类了:
--多重继承,创建新的类
NamedAccount = createClass(Account,Named)
--创建和使用实例
account = NamedAccount:new{name = "abcd"} print(account:getname())
上述的search函数一定程度上影响性能,以下是作者给的改进:
setmetatable(c,{ __index = function ( t,k ) local v = search(k,parents) t[k] = v return v end})
一种编程技巧,谨记!
16.4 Privacy
隐私
在已提到的对对象的设计中,并没有提供隐私机制。这是我们使用table来表现对象的结果,也是受影响与Lua本身排斥一些冗余、人为限制的功能。作者的建议是假如不想访问某些值,那么大可以不去访问就是。
Lua的目标是为开发者提供便利,提供多种技巧实现多数需求,尽管设计lua中的对象初衷是不提供隐私机制的,但是可以通过别的方法来实现这个需求——访问控制。这个用的比较少,但还是值得去了解和学习掌握的。
实现这个功能需求在于用两个table来表现对象:一个表示其状态,一个用来表示其操作行为。访问对象的时候通过第二个table进行访问,而对第一个table的设计也有一定的要求,该table并不是存储在别的table中,而是存储在该对象方法的closure中。以此重新设计Account
e.g. function newAccount( initialBalance ) local self = {balance = initialBalance} local withdraw = 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。然后该函数内部创建了对象的一系列方法。最后函数创建并返回了另外一个对象,该对象内部存储了实际上要实现的方法的名字。返回的这个新的table应该相当于上文提到的第二个table。这里的核心点在于:这些方法没有使用冒号操作符得到self这个额外的默认参数,而是直接使用了。现在我们可以以一下方式创建新的对象并使用其方法:
e.g. acc1 = newAccount(100.00) acc1.withdraw(40.00) print(acc1.getBalance())
利用这种方式创建的table,我们是没有办法直接访问原table的,只能通过newAccount里面的方法来访问。这样就实现来我们想要的隐私功能。
16.5 The Single-Method Approach
单例的实现
e.g. print("The Single-Method Approach \n") 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")) d("set",10) print(d("get"))
没有实例,直接通过对象本身访问对象实现的方法。