上一篇文章简单介绍了 Lua 中的元表和元方法,那么在这里就接着重点讲解一下 Lua 中的 index 和 newindex 元方法。
- **"index": **索引
table[key]
。当table
不是表或是表table
中不存在key
这个键时,这个事件被触发。此时,会读出table
相应的元方法。尽管名字取成这样,这个事件的元方法其实可以是一个函数也可以是一张表。如果它是一个函数,则以table
和key
作为参数调用它。如果它是一张表,最终的结果就是以key
取索引这张表的结果。(这个索引过程是走常规的流程,而不是直接索引,所以这次索引有可能引发另一次元方法。)- **"newindex": **索引赋值
table[key] = value
。和索引事件类似,它发生在table
不是表或是表table
中不存在key
这个键的时候。此时,会读出table
相应的元方法。同索引过程那样,这个事件的元方法即可以是函数,也可以是一张表。如果是一个函数,则以table
、key
、以及value
为参数传入。如果是一张表,Lua 对这张表做索引赋值操作。(这个索引过程是走常规的流程,而不是直接索引赋值,所以这次索引赋值有可能引发另一次元方法。)一旦有了 "newindex" 元方法,Lua 就不再做最初的赋值操作。(如果有必要,在元方法内部可以调用rawset
来做赋值。)
上面对 index 和 newindex 元方法的概念已经说得很清楚,在这就不再重复了。
rawget 和 rewset 方法
rawget (table, index)
在不触发任何元方法的情况下获取 table[index]
的值。table
必须是一张表;index
可以是任何值。
rawset (table, index, value)
在不触发任何元方法的情况下将 table[index]
设为 value
。table
必须是一张表,index
可以是 nil 与 NaN 之外的任何值。value
可以是任何 Lua 值。这个函数返回 table
。
index 元方法
当访问一个 table 中不存在的字段时,会去查找一个叫 index 的元方法。如果没有这个元方法,那么访问结果就为nil,否则,就有这个元方法来提供最终结果。
Window = {}
Window.prototype = {x = 0, y = 0, width = 100, height = 100}
Window.mt = {}
function Window.new(o)
setmetatable(o, Window.mt)
return o
end
Window.mt.__index = function (table, key)
print("call __index function", table, key)
return Window.prototype[key]
end
w = Window.new({x = 10, y = 20})
print("w", w)
print("w.x =", w.x)
print("w.y =", w.y)
print("----------\n")
print("rawget w.x =", rawget(w, "width"))
print("w.width =", w.width)
print("w.height =", w.height)
print("----------\n")
print("set w.width = 555")
w.width = 555
print("w.width =", w.width)
执行上面代码会输出:
w table: 0x7fc932c08b70
w.x = 10
w.y = 20
----------
rawget w.x = nil
call __index function table: 0x7fc932c08b70 width
w.width = 100
call __index function table: 0x7fc932c08b70 height
w.height = 100
----------
set w.width = 555
w.width = 555
index 元方法不必一定是一个函数,它还可以是一个 table。当他是一个 table 时,Lua 就以相同的方式来重新访问这个table。
Window = {}
Window.prototype = {x = 0, y = 0, width = 100, height = 100}
Window.mt = {}
function Window.new(o)
setmetatable(o, Window.mt)
return o
end
Window.mt.__index = Window.prototype
w = Window.new({x = 10, y = 20})
print("w.x =", w.x)
print("w.y =", w.y)
print("w.width =", w.width)
print("w.height =", w.height)
print("----------\n")
print("set w.height = 888")
w.height = 888
print("w.height =", w.height)
print("getmetatable(w).__index.height =", getmetatable(w).__index.height)
执行上面代码会输出:
w.x = 10
w.y = 20
w.width = 100
w.height = 100
----------
set w.height = 888
w.height = 888
getmetatable(w).__index.height = 100
总结:一般我们会把一个 table 作为 index 元方法,只有在特殊需求时才会把一个函数作为 index 元方法。将一个 table 作为 index 元方法是一种快捷的、实现单一继承的方式。虽然用函数作为 index 来实现相同功能的开销很大,但函数更加灵活。可以通过函数来实现多重继承、缓存及其他功能。如果我们想在不触发 index 元方法的情况下访问一个 table,可以使用 rawget 方法。
newindex 元方法
当对一个 table 中不存在的索引赋值时,解释器就会查找 newindex 元方法。如果有这个元方法,解释器就调用它,而不是执行赋值。如果这个元方法是一个 table,解释器就在此 table 中执行赋值,而不是对原来的 table。调用 rawset(t, k, v) 就可以不涉及任何元方法而直接设置 table t 中与 key k 相关联的 value v。
t = {a = 100}
mt = {}
mt.__newindex = function (t, key, value)
print("call __newindex function", t, key, value)
end
setmetatable(t, mt)
print("set t.a = 666")
t.a = 666
print("set t.b = 888")
t.b = 888
print("----------\n")
print([=[rawset(t, "c", 999)]=])
rawset(t, "c", 999)
执行上面代码会输出:
set t.a = 666
set t.b = 888
call __newindex function table: 0x7f8842500000 b 888
----------
rawset(t, "c", 999)
ipairs 和 pairs
t = {1, 2, a = "a"}
mt = {}
-- mt.__index = function (t, key)
-- print("call __index function", t, key)
-- end
mt.__index = {111, 222, 333, 444, a = "aaa", b = "bbb"}
setmetatable(t, mt)
-- 从下列代码的打印可以看出,ipairs 会触发 __index 元方法
print("--- ipairs --")
for i,v in ipairs(t) do
print(i,v)
end
print("")
-- 从下列代码的打印可以看出,pairs 不会触发 __index 元方法
print("--- pairs --")
for k,v in pairs(t) do
print(k,v)
end
执行上面代码会输出:
--- ipairs --
1 1
2 2
3 333
4 444
--- pairs --
1 1
2 2
a a
从以上输出可以看出:ipairs 会触发 index 元方法;而 pairs 不会触发 index 元方法。
简单应用实例
具有默认值的 table
local key = {} -- 唯一的 key
local mt = {__index = function (t) return t[key] end}
function setDefault(t, d)
t[key] = d
setmetatable(t, mt)
end
t = {x = 10, y = 10}
print(t.x, t.z) --> 10 nil
setDefault(t, 100)
print(t.x, t.z) --> 10 100
跟踪 table 的访问
index 和 newindex 都是在 table 中没有所需访问的 index 时才发挥作用的。因此,只有将一个 table 保持为空,才有可能捕捉到所有对它的访问。为了监视一个 table的所有访问,就应该为真正的 table 创建一个代理。这个代理就是一个空的 table,其中index 和 newindex 元方法可用于跟踪所有的访问,并将访问重定向到原来的 table 上。
如果想要同时监视几个 table,无须为每个 table 创建不同的元表。相反,只要以某种形式将每个代理与其原 table 关联起来,并且所有代理都共享一个公共的元表。
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)
t[1] = "hello"
print(t[1])
t["k"] = "v"
print(t["k"])
执行以上代码会输出:
*update of element 1 to hello
*access to element 1
hello
*update of element k to v
*access to element k
v
只读 table
function readOnly(t)
local proxy = {}
local mt = {
__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"}
print(days[1]) --> Sunday
days[2] = "Noday" --> error: attempt to update a read-only table
总结
这里通过一些代码和实例讲解了 Lua 中的 index 和 newindex 元方法,但其实 index 元方法最常见的用途是用来实现面向对象编程中的类、继承和多重继承。后面会有单独的文章介绍怎么 Lua 中实现面向对象编程。
本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/lua-index-metafunc.html