第13章 Metatables and

13 Metatables and
Metamethods
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 (描述其私有行为)。
13.1 算术运算的Metamethods
这一部分我们通过一个简单的例子介绍如何使用 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
13.2 关系运算的Metamethods
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
13.3 库定义的Metamethods
在一些库中,在自己的 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
13.4 表相关的Metamethods
关于算术运算和关系元运算的 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
 
 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值