对于项目的Lua的内容进行梳理,有些设计只是使用,但没有明白为嘛这样写。《Lua程序设计(第四版)》再读了一次就豁然理解了。
元表是面对对象领域中的受限类。像类一样,元表定义是实例的行为。不过,由于元表给出了预定义的操作集合的行为,所以元表比类更受限;同时,元表也不支持继承。lua语言中的每个值都可以有元素了。每个表和用户数据类型都具有了各自独立的元表,而其他类型的值则共享对应的类型所属的同一个元素。
可以使用函数setmetatable来设置或修改任意表的元表
在Lua语言中,我们只能为表设置元表;如果要为其他类型的值设置元表,则必须通过C代码或者调试库完成(该限制存在的主要原因是为了防止过度使用对某类型的所有值生效的元表。Lua语言老版本中的经验表明,这样的全局设置经常导致不可重用的代码)。字符串标准库为所有的字符串都设置了同一个元表,而其他类型在默认情况中都没有元表。
一个表可以成为任意值的元表;一组相关的表也可以共享有一个描述了它们共同行为的通用表;一个表还可以成为它自己的元表。用于描述其自身特性有的行为。
还有位操作和关系运算相关的元方法。注意:__是两个下划线
模式 | 描述 |
---|---|
__add | 对应的运算符’+’ |
__sub | 对应的运算符’-’ |
__mul | 对应的运算符’*’ |
__div | 对应的运算符’/’ |
__mod | 对应的运算符’%’ |
__unm | 对应的运算符’-’ |
__concat | 对应的运算符’…’ |
__eq | 对应的运算符’==’ |
__lt | 对应的运算符’<’ |
__le | 对应的运算符’<=’ |
__band | 按位与 |
__bor | 按位或 |
__bxor | 按位异或 |
__bnot | 按位取反 |
__shl | 向左位移 |
__shr | 向右位移 |
Lua语言会按照如下步骤来查找元方法:如果第一个值有元表且元表中存在所需要的元方法,那么Lua语言就是用这个元方法,与第二个值无关;如果第二个值有元表且元表中存在所需的方法,Lua语言就是用这个元方法;否则,Lu语言就抛出异常。
库定义的元方法
函数tostring就是一个典型的例子。函数print总是调用tostring来进行格式化输出。不过,当对值进行格式化时,函数tostring会首先检查值是否有一个元方法__tostring。如果有,函数tostring就调用这个元方法来完成工作,将对象作为参数传给该函数,然后把元方法的返回值作为函数tostring的返回值。
函数setmetatable和getmetatable也用到了元方法,用于保护元表。想要保护集合,就要使用户既不能看到也不能修改集合的元表。如果在元表中设置了__metatable字段,那么getmetatable会返回这个字段的值,而setmetatable就会引发一个错误。
表相关的元方法
Lua语言还提供了一种改变表在两种正常情况下的行为的方式,即访问和修改表中不存在的字段。
在Lua语言中,使用元方法__index来实现继承是很普遍的方法。虽然被叫作元方法,但元方法__index不一定必须是一个函数,它还还可以是一个表。当元方法是一个函数时,Lua语言会以表和不存在的键为参数调用改函数。
将一个表作__index元方法为实现单继承提供了一种简单快捷的方法。虽然将函数调用作元方法开销更昂贵,但函数却更加灵活了;我们可以通过函数来实现多继承、缓存以及其他的一些变体。
在访问一个表时不调用__index元方法,那么可以使用函数rawget。调用rawget(t,i)会对表t进行原始(raw)的访问。即在不考虑元表的情况下对表进行简单的访问。
__newindex用于表的更新,__index用于表的查询。当对一个表中不存在的索引赋值的时,解释器就会查找__newindex元方法;如果这个元方法存在,那么解释器就调用它而不执行赋值。像元方法存在,那么解释器就调用它而不执行赋值。像元方法__index一样,如果这个元方法是一个表,解释器就在此表中执行赋值,而不是原始的表中进行赋值。
调用rawset(t,k,v)来等价于t[k]=v,但不涉及任何元方法。
结合使用元方法__index和__newindex可以实现Lua语言中的一些强大的结构,列如只读的表、具有默认值的表和面对对象编程中的继承。
Lua语言中的一张表就是一个对象。首先,表与对象一样,可以拥有状态。其次,表与对象一样,拥有一个与其值无关的标识(self);
表与对象一样,具有与创建者和被创建者位置无关的生命周期。另一种更加有原则的方法是对操作的接受者进行操作。因此我们的方法需要一个额外的参数来表示该接受者,这个参数通常被称为self或this;
使用参数self是所有面对对象语言的核心点。Lua语言同样可以使用冒号操作符(colon operator)隐藏该参数。冒号的作用是在一个方法调用中增加一个额外的参数,或在方法定义中增加的隐藏形参。
有两个对象A和B,要让B成为A的一个原型,只需要:setmetatable(A,{__index =b})
可以重新定义从基类继承的任意方法。只需要编写一个新方法就行了。
Lua语言中的对象有一个有趣的特性,就是无须为了指定一种新行为而创建一个新类。
多重继承意味着一个类可以具有多个超类。因此,我们不应该使用一个超类来中的方法来创建子类,而是应该定义一个独立的函数createClass来创建子类。函数CreateClass的参数为新类的所有超类。尤其是一个类不能同时成为其实例和子类的元表。
-- 在表'plist'的列表中查找'k'
local function search(k,plist)
for i=1,#plist do
local v = plist[i][k] -- 尝试第‘i’个超类
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 -- 返回新类
end
由于这种搜索具有一定的复杂性,因此多重继承的性能不如单继承。
私有性是一门面向对象语言不可或缺的部分:每一个对象的状态都应该由它自己控制。
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
面向对象编程实现一个的特例是对象只有一个方法的情况,可以不用创建接口表,只要将这个单独的方法以对象的表形式返回即可。
单方法对象的另一种有趣的情况是,在这个方法其实是一个根据不同的参数完成不同任务的分发方法
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
实现私有性的另一种有趣方式是使用对偶表示,把表当作键,同时又把对象本身当作这个表的键;
这样关键在于:我们不仅可以通过数值和字符串来索引一个表,还可以通过任何值来索引一个表,尤其是可以使用其他的表来索引一个表。