Q:如何定义访问”table”相关的”metamethods”?
A:访问”table”相关的”metamethods”有两个,__index
和__newindex
。
1、之前说过,当访问一个”table”中不存在的域时,返回结果是nil
。这是正确的,但并不是完全正确。实际上当这种情况发生时,Lua会试图寻找对象的”metatable”中名为__index
的”metamethod”。如果没有这个”metamethod”,那么返回nil
,否则由这个__index
负责返回结果。
之前在“快速掌握Lua 5.3 —— 函数”的“附加 4”中提到过一种自动设定默认值的方法。那种方法是在函数内部帮你补填好默认值,但是从你创建的”table”中无法获取函数内部提供的默认值。而现在有了__index
,实现方法就更加灵活,我们可以实现创建一张相当于带有默认值的”table”,
-- 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 = {}
-- 为所创建的"table"分配"metatable"。
function Window.new (o)
setmetatable(o, Window.mt)
return o
end
--[[ 定义"metatable"返回"Window.prototype"中存储的默认值。
当"__index"被调用时,
参数"table"是"w",参数"key"是"width"。]]
Window.mt.__index = function (table, key)
return Window.prototype[key]
end
w = Window.new{x=10, y=20}
print(w.width) --> 100
---------------------------
__index
不像其他”metamethod”一样需要是个函数,它可以是一张”table”。
当他是个函数时,Lua调用它,以”table”和缺失的”key”作为参数(就像上面例子中那样)。而当他是一个”table”时,Lua直接以缺失的”key”作为它的”key”再次访问他(相当于拿着缺失的”key”在它这张”table”中寻找),所以上面的例子中定义__index
的部分可以改为,
Window.mt.__index = Window.prototype
达到的效果是相同的。
2、__newindex
与__index
的功能是互补的关系。当向一个”table”中存入之前不存在的元素时__newindex
被调用(当你向”table”中存储一个之前不存在的”key-value”时,Lua首先会查找对象的”metatable”中的”__newindex”域,如果找到了则调用它,否则进行正常的存入操作)。
-- 继续上面的例子。
Window.mt.__newindex = function(t, k, v) Window.prototype[k] = v end
w["z"] = 30
print(Window.prototype.z) --> 30
print(w.z) --> 30
与__index
的特性相同,如果__newindex
是一个函数,Lua以”table”,”key”,”value”作为参数调用它(就像上面例子中那样)。而如果是一个”table”,Lua在这张”table”上做正常的存入操作,所以__newindex
的部分更改为,
Window.mt.__newindex = Window.prototype
是相同的效果。
Q:如何监控对”table”的操作?
A:__index
和__newindex
均是在”table”中没有指定的”key”时起作用,如果我们想监视对”table”的所有操作,唯一的方法是将”table”一直保持为空。所以我们需要这样的一个”table”,为其分配”metatable”并设置__index
和__newindex
,在这两个”metamethod”内部将”key-value”传递给真正的”table”,或者从真正的”table”中取出”key-value”。
t = {} -- original table (created somewhere)
local _t = t -- keep a private access to original table
t = {} -- create proxy
-- create metatable
local mt = {
__index = function (t,k)
io.write("*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[2] = 'hello' --> *update of element 2 to hello
print(t[2]) --> *access to element 2, hello
不幸的是这种方法不支持表的遍历。当使用pairs()
时,遍历的是那张代理的空表,而不是原表本身。
如果我们想监视许多的”table”,我们不需要为每个代理”table”都分配一个”metatable”。我们可以让每个代理”table”与他们对应的”table”相关连,而这些代理”table”共享一个”metatable”,关联的方法是将原”table”保存在代理”table”中。如果担心域名冲突,还可以使用一个{}
作为索引,
local index = {} -- create private index
-- create metatable
local mt = {
__index = function (t,k)
io.write("*access to element " .. tostring(k) .. ", ")
--[[ 这里传入的"t"是"proxy",如果还是通过"t[index]"的方式获取原"table",
那么这个获取的过程还是会被监视,又会调用"__index",进入无限循环。
所以获取原"table"的时候需要绕过"__index"。
下面"__newindex"中同理。]]
return rawget(t, index)[k] -- access the original table
end,
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
rawget(t, index)[k] = v -- update original table
end
}
function track (t)
local proxy = {}
proxy[index] = t -- 将原"table"存储在代理"table"中,以"{}"为"key"。
setmetatable(proxy, mt)
return proxy
end
table = track{}
table[2] = 'hello' --> *update of element 2 to hello
print(table[2]) --> *access to element 2, hello
Q:如何实现只读”table”?
A:使用代理”table”的概念很容易实现只读”table”,我们只需要监测到更新”table”的操作时报错。如果我们不需要监测取数据操作,我们可以将__index
指定为原”table”,这样将更有效率。
function readOnly (t)
local proxy = {}
local mt = {
--[[ 因为操作的是代理"table",其中没有任何数据,
所以取数据还是要去原"table"。]]
__index = t,
__newindex = function (t,k,v)
-- 这里第二个参数,指定报错的位置是更新操作本身。
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(days[1]) --> Sunday
days[2] = "Noday" --> attempt to update a read-only table
附加:
1、如果一个”table”的”metatable”设定了__index
和__newindex
,而我们在向”table”中存入”key-value”以及从”table”中取出”key-value”时不想触发__index
和__newindex
,使用rawset()
和rawget()
可以绕过他们的操作,
table = {}
new_value = {}
setmetatable(table, {__newindex = new_value, __index = new_value})
table["x"] = 90
print(rawget(table, "x")) --> nil
print(new_value.x) --> 90
rawset(table, "y", 10)
print(rawget(table, "y")) --> 10
print(new_value.y) --> nil
2、有了__index
,将一个”table”中未初始化元素的默认值由nil
更改为0
也就非常的简单了,
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
这段程序为每个需要默认值的”table”创建了一个”metatable”,注意,是在setDefault()
内部创建的,也就是说每个需要默认值的”table”都有一个自己独有的”metatable”,这样对于有许多需要默认值的”table”来说开销会非常大(那得有很多单独的”metatable”被创建)。
显然默认值是与”table”相关连的,这样我们其实可以将默认值存储在他们对应的”table”中(比如default
域,不过这样可能造成域名冲突,如果你不想让这个默认值域与其他的域发生有可能的冲突,你可以使用一个特殊的域名,比如___
。如果这样你依旧不放心的话,你可以像下面的程序那样,使用一个”table”作为”key”),然后让所有”table”共享一个”metatable”,这个”metatable”中有公用的返回默认值的方法。于是程序更改如下,
local key = {} -- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
t[key] = d
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