文章目录

  • 表相关的 Metamethods
  • The ____index Metamethod
  • The ____new index Metamethod
  • 有默认值的表
  • 监控表
  • 只读表


表相关的 Metamethods

关于算术运算和关系元算的 metamethods 都定义了错误状态的行为,他们并不改变语言本身的行为。针对在两种正常状态:表的不存在的域的查询和修改,Lua 也提供了改变 tables 的行为的方法。

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),但有些时候我们需要他,在后面我们将会看到。

The ____new index Metamethod

__newindex metamethod 用来对表更新,__index 则用来对表访问。当你给表的一个缺少的域赋值,解释器就会查找__newindex metamethod:如果存在则调用这个函数而不进行赋值操作。
像__index 一样,如果 metamethod 是一个表,解释器对指定的那个表,而不是原始的表进行赋值操作。
另外,有一个 raw 函数可以绕过 metamethod:调用rawset(t,k,v)不掉用任何 metamethod 对表 t 的 k 域赋值为 v。
__index 和__newindex metamethods 的混合使用提供了强大的结构:从只读表到面向对象编程的带有继承默认值的表。
在这一张的剩余部分我们看一些这些应用的例子,面向对象的编程在另外的章节介绍。

有默认值的表

在一个普通的表中任何域的默认值都是 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 章。

监控表

__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)。

只读表

采用代理的思想很容易实现一个只读表。我们需要做得只是当我们监控到企图修改表时候抛出错误。通过__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