通常Lua中,每个值都有一套预定义的操作集合。这个集合叫做元表。
我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。
如两个table相加。
元方法是元表中的一个值,这个值应该是一个函数。
Lua中的每个值都有一个元表。table和userdata可以有各自独立的元表,而其他类型的值则共享其
类型所属的单一元表。Lua在创建新的table时不会创建元表:
t = {}
print(getmetatable(t)) —> nil
但是可以使用setmetatable来设置或修改任何table的元表。
任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表描述了
它们共同的行为。一个table甚至可以作为它自己的元表,用于描述其特有的行为。
在Lua代码中,只能设置table的元表。若要设置其他类型值的元表,则必须通过C代码来完成。在标
准字符串程序库中为所有的字符串都设置了同一个元表,而其他类型在默认情况下都没有元表。
13.1 算术类的元方法
通过示例说明如何使用元表。假设用table来表示集合,并且有一些函数用来计算集合的并集和交集等。
将这些函数存入一个名为Set的table中。
Set = {}
function Set.new(list)
local set ={}
for k, v inpairs(list) do set[v] = true end
return set
end
function Set.union(a, b)
local res =Set.new{}
for k inpairs(a) res[k] = true end
for k inpairs(b) res[k] = true end
return res
end
function Set.intersection(a, b)
local res =Set.new{}
for k inpairs(a) do
res[k] =b[k]
end
return res
end
--打印集合的函数
function Set.tostring(set)
local list ={}
for e inpairs(set) do
list[#list+ 1] = e
end
return"{" .. table.concat(list, ",") .. "}"
end
function Set.print(s)
print(Set.tostring(s))
end
假设使用加号来计算两个集合的并集,那么就需要让所有用于表示集合的table共享一个元表,
并且在该元表中定义如何执行一个加法操作。
第一步,创建一个常规的table用作集合的元表:
local mt = {}
第二步,修改Set.new函数,将mt设置为当前所创建的table的元表.
functionSet.new(list)
local set= {}
setmetatable(set,mt)
for k, vin pairs(list) do set[v] = true end
return set
end
最后,将元方法加入元表中。
mt.__add =Set.union
还可以用乘号来求集合的交集: mt.__mul = Set.intersection
在元表中,每种算术操作符都有对应的字段名。除了__add 和 __mul外,还有__sum(减法)、
__div(除法)、__unm(相反数)、__mod(取模)和__pow(乘幂),还有__concat用于描述连接操作符的行为。
当两个集合相加时,可是使用任意一个集合的元表。但当两个相加的类型其中一个没有元表时,Lua会
按照如下步骤查找元表:
如果一个值有元表,并且元表中有__add字段,那么Lua就以这个字段为元方法,而与第二个值无关;
反之,如果第二个值有元表并含有__add字段,Lua就以此字段为元方法;如果有两个值都没有元方法,
Lua就会引发一个错误。
13.2 关系类的元方法
元表还可以指定关系操作符的含义,元方法为__eq(等于)、__lt(小于)、和__le(小于等于)。而其它3个关系
操作符则没有单独的元方法。
Lua会将 a ~= b转化为not (a==b),将a > b转化为 b < a,将a >= b 转化为 b <= a.
在集合操作中 <= 通常表示集合间的包含关系: a <=b通常意味着a是b的一个子集。
根据这样的提示,仍有可能得到 a <= b 和 b <a 同时为假的情况。
因此需要分别为__le(小于等于) 和 __lt(小于) 提供实现:
mt.__le = function(a, b)
for k inpairs(a) do
if notb[k] then return false end
end
return true
end
mt.__lt = function(a, b)
return a<= b and not (b <= a)
end
还可以定义集合的相等性判断:
mt.__eq = function(a, b)
return a <= b and b <= a
end
有了这些定义后,就可以比较集合了。
注意:与算术类元方法不同的是,关系类的元方法不能应用于混合的类型。如果试图将一个字符串
与一个数字作顺序性比较,Lua会引发一个错误。同样,如果试图比较有个具有不同元方法的对象,
Lua也会引发一个错误。
等于比较永远不会引发错误。但如果两个对象拥有不同的元方法,那么等于操作不会调用任何一个
元方法,而是直接返回false。只有当两个比较对象共享一个元方法时,Lua才会调用这个等于比较的元方法。
13.3 库定义的元方法
在操作某个值时,Lua虚拟机首先会检测这个值是否有元表,元表中是否定义了关于此操作的元方法。
由于元表也是一种常规的table,所以任何人,任何函数都可以使用它们。函数tostring就是一个典型的实例。
函数print总是调用tostring来格式化其输出。当格式化任意值时,tostring会检查该值的元表中是否有一个
__tostring的元方法。如果有这个元方法,tostring就用该值作为参数来调用这个元方法。接下来由这个元方法
完成实现的工作,它返回的结果也就是tostring的结果。
设置示例元表中的__tostring字段:
mt.__tostring = Set.tostring
这样只要调用print来打印集合,print就会调用tostring函数,进而调用到Set.tostring:
s1 = Set.new{10, 4, 5}
print(s1) --> {4, 5, 10}
函数setmetatable 和 getmetatable 也会用到元表中的一个字段,用于保护元表。假设
想要保护集合的元表,使用户既不能看也不能修改集合的元表,那么就需要用到字段__metatable。
当设置了该字段时,getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误。
13.4 table访问的元方法
算术类和关系类运算符的元方法都为各种错误情况定义了行为,它们不会改变语言的常规
行为。但Lua还提供了可以改变table行为的方法:查询table及修改table中不存在的字段。
13.4.1
当访问一个table中不存在的字段时,得到的结果为nil。这是对的,但并不完全正确。
这种访问会促使解释器去查找一个叫__index的元方法。如果没有这个元方法,访问结果就是nil。
否则就由这个元方法来提供最终的结果。
一个有关继承的典型示例。要创建一些描述窗口的table,每个table中必须描述一些窗口参数,
例如位置、大小及主题颜色等,这些参数都有默认值。在创建窗口对象时还可以仅指定那些不同于默认值的参数。
第一种方法是使用一个构造式,在其中填写那些不存在的字段。
第二种方法是让新窗口从一个原型窗口处继承所有不存在的字段。首先,声明一个原型和一个构造函数,构造函数创建新的窗口,并使他们共享同一个元表:
Window = {}
Window.prototype = {x=0, y=0, width=100, height=100} -- 使用默认值创建原型
Window.mt = {} --创建元表
function Window.new(o) --声明构造函数
setmetatable(o,mt)
return o
end
定义 __index 元方法:
Window.mt.__index = function(table, key)
returnWindow.prototype[key]
end
创建一个新窗口,查询一个它没有的字段:
w = Window.new{x=10, y=20}
print(w.width) ---> 100
在Lua中,将__index元方法用于继承是很普遍的方法,但Lua还提供了一种更便捷的方式来实现此功能。
__index元方法不一定是一个函数,它还可以是一个table。当它是一个函数时,Lua以table和一个不存在
的key作为参数来调用该函数;当它是一个table时,Lua就以相同的方式来重新访问这个table。因此,
前例中__index的声明可以简单地写为:
Window.mt.__index = Window.prototype
将一个table作为__index的元方法是一种快捷的、实现单一继承的方式。虽然将函数作为__index来实现相
同功能的开销较大,但函数更加灵活。可以通过函数来实现多重继承、缓存及其它一些功能。
第16章中详细讨论。
如果不想在访问一个table时涉及到它的__index元方法,可以使用函数rawget.调用rawget(t,i)就是对table进
行了一个原始的访问,也就是一次不考虑元表的简单访问。但一次原始访问并不会加速代码的执行。
13.4.2 __newindex 元方法
__newindex元方法与__index类似,不同之处在于前者用于table的更新,而后者用于table的查询。当对一
个table中不存在的索引赋值时,解释器就会查找__newindex元方法。如果有这个元方法,解释器就调用它,
而不是执行赋值。如果这个元方法是一个table,解释器就在此table中执行赋值,而不是对原来的table。
此外还有一个原始函数允许绕过元方法:调用rawset(t,k,v)就可以不涉及任何元方法而直接设置table t中的key k
相关联的value v。
组合使用__index和__newindex元方法可以实现出Lua中的强大功能,如:只读的table、具有默认值的table和
面向对象编程中的继承。
13.4.3 具有默认值的table
常规table中的任何字段默认都是nil.通过元表可以很容易地修改这个默认值:
function setDefault(t, d)
local mt ={__index = function() return d end}
setmetatable(t,mt)
end
tab = {x=10, y=20}
print(tab.x, tab.z) -->10 nil
setDefault(tab, 0)
print(tab.x, tab.z) -->10 0
setDefault函数为所有需要默认值得table创建了一个新的元表。但如果创建很多需要不同默认值的table,
这种方法的开销就比较大。这时,可以使用一个额外的字段来保持默认值,如可以使用"___"这个key
作为额外的字段:
local mt = {__index = function(t) return t.___ end}
function setDefault(t, d)
t.___ = d
setmetatable(t,mt)
end
如果担心名称冲突,那么要确保这个特殊key的唯一性,只需创建一个新的table并用它作为key即可:
local key = {}
local mt = {__index = function(t)return t[key] end}
function setDefault(t, d)
t[key] = d
setmetatable(t, mt)
end
还有一种方法可以将table与其默认值关联起来:使用一个独立的table,它的key为各种table,value就是
各个table的默认值。(需要使用弱引用,见17章)
还有一种备忘录元表的方法,它能使具有相同默认值的table复用同一个元表。(需要使用弱引用)
13.4.4 跟踪table的访问
__index 和 __newindex 都是在table中没有所需访问的index时才发挥作用。因此只有将一个table保持为空,
才有可能捕捉到所有对它的访问。为了监视一个table的所有访问,就应该为真正的table创建一个代理。
这个代理就是一个空的table,其中__index和__newindex元方法可用于跟踪所有的访问,并将访问重新
定向到原来的table上。示例——跟踪table t 的访问:
t = {} -- 原来的table
local _t = t --保持对原table的一个私有访问
t = {} -- 创建代理
local mt = {
__index =function(t, k)
print("access to element"..tostring(k))
return_t[k] -- 访问原来的table
end,
__newindex =function(t, k, v)
print("update of element"..tostring(k).." to "..tostring(v))
_t[k] = v --更新原来的table
end
}
setmetatable(t,mt)
但这种方法无法遍历原来的table。函数pairs只能操作代理table,无法访问原来的table。
如果要同时监视几个table,无须为每个table创建不同的元表。相反,只要以某种形式将每个代理
与其原table关联起来,并且所有代理都共享一个公共的元表。例如将原来的table保存在代理table的
一个特殊字段中。代码如下:
local index = {} -- 创建私有索引
local mt = {
__index =function(t, k)
print("access toelement "..tostring(k))
returnt[index][k] -- 访问原来的table
end,
__newindex =function(t, k, v)
print("updateof element " ..tostring(k).." to "..tostring(v))
t[index][k]= v -- 更新原来的table
end
}
function track(t)
local proxy ={}
proxy[index]= t
setmetatable(proxy,mt)
return proxy
end
现在要监视table t,唯一要做的就是执行: t =track(t)。
13.4.5 只读的table
通过代理的概念,可以很容易地实现出只读的table。只需要跟踪所有对table的更新操作,并引发一个
错误就可以。如果无须跟踪查询访问,对于__index元方法可以直接使用原table来代替函数。这种做法简单,
效率高,但是要求为每个只读代理创建一个新的元表,其中__index指向原来的table。示例代码:
function readOnly(t)
local proxy ={}
local mt = {
__index =t,
__newindex= function(t, k, v)
error("attemptto update a read-only table", 2)
end
}
setmetatable(proxy,mt)
return proxy
end
使用示例,创建了一个表示星期的只读table:
days =readOnly{"Sunday","Monday", "Tuesday","Wednesday", "Thursday", "Friday","Saturday"}
print(days[1]) --> Sunday
days[2] = "Noday" --> stdin:1:1 attempt to update a read-only table