关于算术运算和关系元运算的metamethods都定义了错误状态的行为,他们并不改变语言本身的行为。针对在两种正常状态:表的不存在的域的查询和修改,Lua也提供了改变tables的行为的方法。元方法是元表的方法。
_index
当我们访问一个表的不存在的域,返回结果为nil,这是正确的,但并不一定正确。实际上,这种访问触发lua解释器去查找__index metamethod:如果不存在,返回结果为nil;如果存在则由__index metamethod返回结果。
这个例子的原型是一种继承。假设我们想创建一些表来描述窗口。每一个表必须描述窗口的一些参数,比如:位置,大小,颜色风格等等。所有的这些参数都有默认的值,当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口。第一种方法是,实现一个表的构造器,对这个表内的每一个缺少域都填上默认值。第二种方法是,创建一个新的窗口去继承一个原型窗口的缺少域。首先,我们实现一个原型和一个构造函数,他们共享一个metatable:
Window = {} --创建一个命名空间 Window.prototype = {x=0, y=0, width=100, height=100} --使用默认值来创建一个原型 Window.mt = {} --创建元表 function Window.new(t) --声明构造函数 setmetatable(t,Window.mt) return t end --定义__index metamethod Window.mt.__index = function(table,key) return Window.prototype[key] end local w = Window.new({x=10,y=20}) print(w.x,w.y,w.width,w.height) --10 20 100 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使用,提供了一种廉价而简单的实现单继承的方法。一个函数的代价虽然稍微高点,但提供了更多的灵活性:我们可以实现多继承,隐藏,和其他一些变异的机制。
当我们想不通过调用__index metamethod来访问一个表,我们可以使用rawget函数。Rawget(t,i)的调用以raw access方式来访问表的
_newindex
__newindex metamethod用来对表更新,__index则用来对表访问。当你给表的一个缺少的域赋值,解释器就会查找__newindex metamethod:如果存在则调用这个函数而不进行赋值操作。像__index一样,如果metamethod是一个表,解释器对指定的那个表,而不是原始的表进行赋值操作。另外,有一个raw函数可以绕过metamethod:调用rawset(t,k,v)不掉用任何metamethod对表t的k域赋值为v。__index和__newindex
metamethods的混合使用提供了强大的结构:从只读表到面向对象编程的带有继承默认值的表。
有默认值得表
function setDefault(t,d) local mt = { __index = function() return d end} setmetatable(t,mt) end local t = {x=10,y=20} print(t.x,t.y) --> 10 20 setDefault(t,0) print(t.x,t.z) -->10 0 现在,不管什么时候我们访问表的缺少的域,他的__index metamethod被调用并返回0 --setDefault函数为每一个需要默认值的表创建了一个新的metatable。在有很多的表需要默认值的情况下,这可能使得花费的代价变大。 --然而metatable有一个默认值d和它本身关联,所以函数不能为所有表使用单一的一个metatable。 --为了避免带有不同默认值的所有的表使用单一的metatable,我们将每个表的默认值,使用一个唯一的域存储在表本身里面。 --如果我们不担心命名的混乱,我可使用像"___"作为我们的唯一的域: local mt = {__index = function(t) return t.__ end} function setDefault2(t,d) t.__ = d setmetatable(t,mt) end --如果我们担心命名混乱,也很容易保证这个特殊的键值唯一性。我们要做的只是创建一个新表用作键值: local key = {} -- unique key local m = {__index = function(t) return t[key] end} function setDefault3(t,d) t[key] = d setmetatable(t,mt) end
另外一种解决表和默认值关联的方法是使用一个分开的表来处理,在这个特殊的表中索引是表,对应的值为默认值。然而这种方法的正确实现我们需要一种特殊的表:weak table.
为了带有不同默认值的表可以重用相同的原表,还有一种解决方法是使用memoize metatables,然而这种方法也需要weak tables.
监控表
__index和__newindex都是只有当表中访问的域不存在时候才起作用。捕获对一个表的所有访问情况的唯一方法就是保持表为空。因此,如果我们想监控一个表的所有访问情况,我们应该为真实的表创建一个代理。这个代理为空表,并且带有__index和__newindex metamethods,由这两个方法负责跟踪表的所有访问情况并将起指向原始的表。
local t = {1,2,3,4,5,6} --要监控的表,又成原始表 local _t = t t = {} --创建一个代理,与原始表同名,需要是个空表 local mt = { --元表 __index = function(t,k) print("*access to element " .. tostring(k)) return _t[k] --访问原始表 end, __newindex = function(t,k,v) print("*update of element " .. tostring(k) .. " to " .. tostring(v)) _t[k] = v --修改原始表 end } setmetatable(t,mt) print(t[2]) -->*access to element 2 --2 t[2] = 45 -->*update of element 2 to 45
注意:不幸的是,这个设计不允许我们遍历表。Pairs函数将对proxy进行操作,而不是原始的表。如果我们想监控多张表,我们不需要为每一张表都建立一个不同的metatable。我们只要将每一个proxy和他原始的表关联,所有的proxy共享一个公用的metatable即可。将表和对应的proxy关联的一个简单的方法是将原始的表作为proxy的域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的key。将上面的思想汇总,最终的结果如下:
local index = {} --创建一个私有访问,需要是个空表 local mt = { __index = function(t,k) print("*access to element " .. tostring(k)) return t[index][k] ----访问原始表 end, __newindex = function(t,k,v) print("*update of element " .. tostring(k) .. " to " .. tostring(v)) t[index][k] = v --修改原始表 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 = {__index = t, __newindex = function(t,k,v) error("attemp to update a read-only table",2) end } setmetatable(proxy,mt) return proxy end local s = {"one","two"} s = readOnly(s) print(s[1] .. s[2]) -->onetwo s[1] = 12 print(s[1]) -->attemp to update a read-only table