Lua
中的
table
由于定义的行为,我们可以对
key-value
对执行加操作,访问
key
对应的
value
,遍历所有的
key-value
。但是我们不可以对两个
table
执行加操作,也不可以比较两个表的大小。
Metatables
允许我们改变
table
的行为,例如,使用
Metatables
我们可以定义
Lua
如何计算两个
table
的相加操作
a+b
。当
Lua
试图对两个表进行相加时,他会检查两个表是否有一个表有
Metatable
,并且检查
Metatable
是否有
__add
域。如果找到则调用这个
__add
函数(所谓的
Metamethod
)去计算结果。
Lua
中的每一个表都有其
Metatable
。(后面我们将看到
userdata
也有
Metatable
),
Lua
默认创建一个不带
metatable
的新表
t = {}
print(getmetatable(t)) --> nil
可以使用
setmetatable
函数设置或者改变一个表的
metatable
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)
任何一个表都可以是其他一个表的
metatable
,一组相关的表可以共享一个
metatable
(描述他们共同的行为)。一个表也可以是自身的
metatable
(描述其私有行为)。
这一部分我们通过一个简单的例子介绍如何使用
metamethods
。假定我们使用
table
来描述结合,使用函数来描述集合的并操作,交集操作,
like
操作。我们在一个表内定义这些函数,然后使用构造函数创建一个集合:
Set = {}
function Set.new (t)
local set = {}
for _, l in ipairs(t) do set[l] = true end
return set
end
function Set.union (a,b)
local res = Set.new{}
for k in pairs(a) do res[k] = true end
for k in pairs(b) do res[k] = true end
return res
end
function Set.intersection (a,b)
local res = Set.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end
为了帮助理解程序运行结果,我们也定义了打印函数输出结果:
function Set.tostring (set)
local s = "{"
local sep = ""
for e in pairs(set) do
s = s .. sep .. e
sep = ", "
end
return s .. "}"
end
function Set.print (s)
print(Set.tostring(s))
end
现在我们想加号运算符
(+)
执行两个集合的并操作,我们将所有集合共享一个
metatable
,并且为这个
metatable
添加如何处理相加操作。
第一步,我们定义一个普通的表,用来作为
metatable
。为避免污染命名空间,我们将其放在
set
内部。
Set.mt = {} -- metatable for sets
第二步,修改
set.new
函数,增加一行,创建表的时候同时指定对应的
metatable
。
function Set.new (t) -- 2nd version
local set = {}
setmetatable(set, Set.mt)
for _, l in ipairs(t) do set[l] = true end
return set
end
这样一来,
set.new
创建的所有的集合都有相同的
metatable
了:
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1)) --> table: 00672B60
print(getmetatable(s2)) --> table: 00672B60
第三步,给
metatable
增加
__add
函数。
Set.mt.__add = Set.union
当
Lua
试图对两个集合相加时,将调用这个函数,以两个相加的表作为参数。
通过
metamethod
,我们可以对两个集合进行相加:
s3 = s1 + s2
Set.print(s3) --> {1, 10, 20, 30, 50}
同样的我们可以使用相乘运算符来定义集合的交集操作
Set.mt.__mul = Set.intersection
Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}
对于每一个算术运算符,
metatable
都有对应的域名与其对应,除了
__add
、
__mul
外,还有
__sub(
减
)
、
__div(
除
)
、
__unm(
负
)
、
__pow(
幂
)
,我们也可以定义
__concat
定义连接行为。
当我们对两个表进行加没有问题,但如果两个操作数有不同的
metatable
例如:
s = Set.new{1,2,3}
s = s + 8
Lua
选择
metamethod
的原则:如果第一个参数存在带有
__add
域的
metatable
,
Lua
使用它作为
metamethod
,和第二个参数无关;
否则第二个参数存在带有
__add
域的
metatable
,
Lua
使用它作为
metamethod
否则报错。
Lua
不关心这种混合类型的,如果我们运行上面的
s=s+8
的例子在
Set.union
发生错误:
bad argument #1 to `pairs' (table expected, got number)
如果我们想得到更加清楚地错误信息,我们需要自己显式的检查操作数的类型:
function Set.union (a,b)
if getmetatable(a) ~= Set.mt or
getmetatable(b) ~= Set.mt then
error("attempt to `add' a set with a non-set value", 2)
end
... -- same as before
Metatables
也允许我们使用
metamethods
:
__eq
(等于),
__lt
(小于),和
__le
(小于等于)给关系运算符赋予特殊的含义。对剩下的三个关系运算符没有专门的
metamethod
,因为
Lua
将
a ~= b
转换为
not (a == b)
;
a > b
转换为
b < a
;
a >= b
转换为
b <= a
。
(直到
Lua 4.0
为止,所有的比较运算符被转换成一个,
a <= b
转为
not (b < a)
。然而这种转换并不一致正确。当我们遇到偏序(
partial order
)情况,也就是说,并不是所有的元素都可以正确的被排序情况。例如,在大多数机器上浮点数不能被排序,因为他的值不是一个数字(
Not a Number
即
NaN
)。根据
IEEE 754
的标准,
NaN
表示一个未定义的值,比如
0/0
的结果。该标准指出任何涉及到
NaN
比较的结果都应为
false
。也就是说,
NaN <= x
总是
false
,
x < NaN
也总是
false
。这样一来,在这种情况下
a <= b
转换为
not (b < a)
就不再正确了。)
在我们关于基和操作的例子中,有类似的问题存在。
<=
代表集合的包含:
a <= b
表示集合
a
是集合
b
的子集。这种意义下,可能
a <= b
和
b < a
都是
false
;因此,我们需要将
__le
和
__lt
的实现分开:
Set.mt.__le = function (a,b) -- set containment
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
Set.mt.__lt = function (a,b)
return a <= b and not (b <= a)
end
最后,我们通过集合的包含来定义集合相等:
Set.mt.__eq = function (a,b)
return a <= b and b <= a
end
有了上面的定义之后,现在我们就可以来比较集合了:
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2) --> true
print(s1 < s2) --> true
print(s1 >= s1) --> true
print(s1 > s1) --> false
print(s1 == s2 * s1) --> true
与算术运算的
metamethods
不同,关系元算的
metamethods
不支持混合类型运算。对于混合类型比较运算的处理方法和
Lua
的公共行为类似。如果你试图比较一个字符串和一个数字,
Lua
将抛出错误。相似的,如果你试图比较两个带有不同
metamethods
的对象,
Lua
也将抛出错误。
但相等比较从来不会抛出错误,如果两个对象有不同的
metamethod
,比较的结果为
false
,甚至可能不会调用
metamethod
。这也是模仿了
Lua
的公共的行为,因为
Lua
总是认为字符串和数字是不等的,而不去判断它们的值。仅当两个有共同的
metamethod
的对象进行相等比较的时候,
Lua
才会调用对应的
metamethod
。
在一些库中,在自己的
metatables
中定义自己的域是很普遍的情况。到目前为止,我们看到的所有
metamethods
都是
Lua
核心部分的。有虚拟机负责处理运算符涉及到的
metatables
和为运算符定义操作的
metamethods
。但是,
metatable
是一个普通的表,任何人都可以使用。
tostring
是一个典型的例子。如前面我们所见,
tostring
以简单的格式表示出
table
:
print({}) --> table: 0x8062ac0
(注意:
print
函数总是调用
tostring
来格式化它的输出)。然而当格式化一个对象的时候,
tostring
会首先检查对象是否存在一个带有
__tostring
域的
metatable
。如果存在则以对象作为参数调用对应的函数来完成格式化,返回的结果即为
tostring
的结果。
在我们集合的例子中我们已经定义了一个函数来将集合转换成字符串打印出来。因此,我们只需要将集合的
metatable
的
__tostring
域调用我们定义的打印函数:
Set.mt.__tostring = Set.tostring
这样,不管什么时候我们调用
print
打印一个集合,
print
都会自动调用
tostring
,而
tostring
则会调用
Set.tostring
:
s1 = Set.new{10, 4, 5}
print(s1) --> {4, 5, 10}
setmetatable/getmetatable
函数也会使用
metafield
,在这种情况下,可以保护
metatables
。假定你想保护你的集合使其使用者既看不到也不能修改
metatables
。如果你对
metatable
设置了
__metatable
的值,
getmetatable
将返回这个域的值,而调用
setmetatable
将会出错:
Set.mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {})
stdin:1: cannot change protected metatable
关于算术运算和关系元运算的
metamethods
都定义了错误状态的行为,他们并不改变语言本身的行为。针对在两种正常状态:表的不存在的域的查询和修改,
Lua
也提供了改变
tables
的行为的方法。
13.4.1 The __index Metamethod
前面说过,当我们访问一个表的不存在的域,返回结果为
nil
,这是正确的,但并不一定正确。实际上,这种访问触发
lua
解释器去查找
__index metamethod
:如果不存在,返回结果为
nil
;如果存在则由
__index metamethod
返回结果。
这个例子的原型是一种继承。假设我们想创建一些表来描述窗口。每一个表必须描述窗口的一些参数,比如:位置,大小,颜色风格等等。所有的这些参数都有默认的值,当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口。第一种方法是,实现一个表的构造器,对这个表内的每一个缺少域都填上默认值。第二种方法是,创建一个新的窗口去继承一个原型窗口的缺少域。首先,我们实现一个原型和一个构造函数,他们共享一个
metatable
:
-- create a namespace
Window = {}
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100, }
-- create a metatable
Window.mt = {}
-- declare the constructor function
function Window.new (o)
setmetatable(o, Window.mt)
return o
end
现在我们定义
__index metamethod
:
Window.mt.__index = function (table, key)
return Window.prototype[key]
end
这样一来,我们创建一个新的窗口,然后访问他缺少的域结果如下:
w = Window.new{x=10, y=20}
print(w.width) --> 100
当
Lua
发现
w
不存在域
width
时,但是有一个
metatable
带有
__index
域,
Lua
使用
w
(
the table
)和
width
(缺少的值)来调用
__index metamethod
,
metamethod
则通过访问原型表(
prototype
)获取缺少的域的结果。
__index metamethod
在继承中的使用非常常见,所以
Lua
提供了一个更简洁的使用方式。
__index metamethod
不需要非是一个函数,他也可以是一个表。但它是一个函数的时候,
Lua
将
table
和缺少的域作为参数调用这个函数;当他是一个表的时候,
Lua
将在这个表中看是否有缺少的域。所以,上面的那个例子可以使用第二种方式简单的改写为:
Window.mt.__index = Window.prototype
现在,当
Lua
查找
metatable
的
__index
域时,他发现
window.prototype
的值,它是一个表,所以
Lua
将访问这个表来获取缺少的值,也就是说它相当于执行:
Window.prototype["width"]
将一个表作为
__index metamethod
使用,提供了一种廉价而简单的实现单继承的方法。一个函数的代价虽然稍微高点,但提供了更多的灵活性:我们可以实现多继承,隐藏,和其他一些变异的机制。我们将在第
16
章详细的讨论继承的方式。
当我们想不通过调用
__index metamethod
来访问一个表,我们可以使用
rawget
函数。
Rawget(t,i)
的调用以
raw access
方式访问表。这种访问方式不会使你的代码变快(
the overhead of a function call kills any gain you could have
),但有些时候我们需要他,在后面我们将会看到。
13.4.2 The __newindex Metamethod
__newindex metamethod
用来对表更新,
__index
则用来对表访问。当你给表的一个缺少的域赋值,解释器就会查找
__newindex metamethod
:如果存在则调用这个函数而不进行赋值操作。像
__index
一样,如果
metamethod
是一个表,解释器对指定的那个表,而不是原始的表进行赋值操作。另外,有一个
raw
函数可以绕过
metamethod
:调用
rawset(t,k,v)
不掉用任何
metamethod
对表
t
的
k
域赋值为
v
。
__index
和
__newindex metamethods
的混合使用提供了强大的结构:从只读表到面向对象编程的带有继承默认值的表。在这一张的剩余部分我们看一些这些应用的例子,面向对象的编程在另外的章节介绍。
13.4.3有默认值的表
在一个普通的表中任何域的默认值都是
nil
。很容易通过
metatables
来改变默认值:
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
现在,不管什么时候我们访问表的缺少的域,他的
__index metamethod
被调用并返回
0
。
setDefault
函数为每一个需要默认值的表创建了一个新的
metatable
。在有很多的表需要默认值的情况下,这可能使得花费的代价变大。然而
metatable
有一个默认值
d
和它本身关联,所以函数不能为所有表使用单一的一个
metatable
。为了避免带有不同默认值的所有的表使用单一的
metatable
,我们将每个表的默认值,使用一个唯一的域存储在表本身里面。如果我们不担心命名的混乱,我可使用像
"___"
作为我们的唯一的域:
local mt = {__index = function (t) return t.___ end}
function setDefault (t, d)
t.___ = d
setmetatable(t, mt)
end
如果我们担心命名混乱,也很容易保证这个特殊的键值唯一性。我们要做的只是创建一个新表用作键值:
local key = {} -- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
t[key] = d
setmetatable(t, mt)
end
另外一种解决表和默认值关联的方法是使用一个分开的表来处理,在这个特殊的表中索引是表,对应的值为默认值。然而这种方法的正确实现我们需要一种特殊的表:
weak table
,到目前为止我们还没有介绍这部分内容,将在第
17
章讨论。
为了带有不同默认值的表可以重用相同的原表,还有一种解决方法是使用
memoize metatables
,然而这种方法也需要
weak tables
,所以我们再次不得不等到第
17
章。
13.4.4监控表
__index
和
__newindex
都是只有当表中访问的域不存在时候才起作用。捕获对一个表的所有访问情况的唯一方法就是保持表为空。因此,如果我们想监控一个表的所有访问情况,我们应该为真实的表创建一个代理。这个代理是一个空表,并且带有
__index
和
__newindex metamethods
,由这两个方法负责跟踪表的所有访问情况并将其指向原始的表。假定,
t
是我们想要跟踪的原始表,我们可以:
t = {} -- original table (created somewhere)
-- keep a private access to original table
local _t = t
-- create proxy
t = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return _t[k] -- access the original table
end,
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
_t[k] = v -- update original table
end
}
setmetatable(t, mt)
这段代码将跟踪所有对
t
的访问情况:
> t[2] = 'hello'
*update of element 2 to hello
> print(t[2])
*access to element 2
hello
(注意:不幸的是,这个设计不允许我们遍历表。
Pairs
函数将对
proxy
进行操作,而不是原始的表。)如果我们想监控多张表,我们不需要为每一张表都建立一个不同的
metatable
。我们只要将每一个
proxy
和他原始的表关联,所有的
proxy
共享一个公用的
metatable
即可。将表和对应的
proxy
关联的一个简单的方法是将原始的表作为
proxy
的域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的
key
。将上面的思想汇总,最终的结果如下:
-- create private index
local index = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return t[index][k] -- access the original table
end
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) .. " to "
.. tostring(v))
t[index][k] = v -- update original table
end
}
function track (t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
现在,不管什么时候我们想监控表
t
,我们要做得只是
t=track(t)
。
13.4.5只读表
采用代理的思想很容易实现一个只读表。我们需要做得只是当我们监控到企图修改表时候抛出错误。通过
__index metamethod
,我们可以不使用函数而是用原始表本身来使用表,因为我们不需要监控查寻。这是比较简单并且高效的重定向所有查询到原始表的方法。但是,这种用法要求每一个只读代理有一个单独的新的
metatable
,使用
__index
指向原始表:
function readOnly (t)
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
(记住:
error
的第二个参数
2
,将错误信息返回给企图执行
update
的地方)作为一个简单的例子,我们对工作日建立一个只读表:
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(days[1]) --> Sunday
days[2] = "Noday"
stdin:1: attempt to update a read-only table