今天有幸学习了LUA语言中可以说是最强大的功能:__index和__newindex。趁着刚看完还没有忘记,赶紧写个笔记冷静一下。
1. 回顾元表(metatable)
在LUA语言中,每个值都有一套预定义的操作集合,我们可以对两个数加减,对字符串连接、删减等等。不过,LUA里面并没有把table变量的操作定义好。可以理解,毕竟table变量内容太复杂,里面既能有数值和字符串,还能有函数甚至另一个table等奇葩的变量。这时候,就需要元表这一助手,来定义两个table之间应该如何操作。
想要设置元表,我们一般使用如下方法:
t1 = {}
setmetatable(t, t1) --t是一个table,而t1是一个元表metatable
如书上所言,任何table都可以作为任何值的元表,一组table可以共享一个通用的元表,table还可以设置成自身的元表。
利用元表和原方法的概念,我们可以自己定义两个table的交集、并集等操作,不过这不是今天的重点,所以就不展开了,有兴趣可以看看相关书籍和blog。
2. __index大法
首先看看如下情况:
local tab = {
name="haha"
}
print(tab.date)
大家一看肯定知道,print访问了tab中并不存在的索引,因此结果必定是nil,这也算是一个LUA语言中的常识了。那么,会不会有例外呢?
答案是有。实际上,当遇到上述的访问了table中并不存在的索引变量时,解释器还会坐另外一步工作:查找__index元方法,一般都把此方法叫做继承。
__index一般都是要由用户自己编写的,根据上面那个例子,我们把程序稍微修改一下:
local tab = {
name = "haha"
}
local mt = {
__index = function(t, key) --定义了访问空索引时如何操作
print("no such an index!")
end
}
setmetatable(tab, mt)
print(tab.date)
以上代码的功能是:如果试图访问tab.date这个不存在的变量,由于__index的存在,将会执行那句print,告诉你没有date这个索引。
当然,我们还可以对其做一些有趣的操作,比如修改默认值:
local mt = {
__index = function(t, key)
return "empty"
end
}
此时,执行print(tab.date)语句的结果便是”empty”,因为我们已经规定了它的行为:对空的索引,返回值一律为”empty”。
还有一种调用的方法,是这样的:
local tab_old = {
name = "haha",
date = "2017.1.7"
}
local tab = {
name = "haha"
}
local mt = {
__index = tab_old
}
setmetatable(tab, mt)
print(tab.date)
当程序试图访问tab.date时,由于并没有这个索引,就去寻找与tab相关的index元方法了,而index就关联到了tab_old这个表格,因此程序便会在tab_old中寻找date索引。
以上只是一些简单的示范,大家还可以开开脑洞,写出其他更多有趣的操作。要注意的是,__index是元方法,因此必须对一个table设置相应的元表才能实现功能。
3. __newindex大法
__newindex就是另一个有趣的元方法了。它和__index略有区别:__index对访问不存在的索引时才会触发,而__newindex对不存在的索引的赋值行为都会触发。咋一看还真是一对好基友。
先看一个简单的例子:
local tab = {
name="haha"
}
tab.date = "2017.1.7"
print(tab.date)
只要执行上述语句,输出的值自然是2017.1.7。可是,上面的赋值行为并不会在终端展现出来,如果我们想监控对tab的任何新赋值操作,就需要__newindex出马了:
--错的,请勿模仿!
local tab = {
name="haha"
}
local mt = {
__newindex = function(t, k, v)
print("Successfully set element \"" .. tostring(k) ..
"\" as " .. tostring(v))
end
}
setmetatable(tab, mt)
tab["date"] = "2017.1.7"
print(tab.date)
切记,__newindex只有在赋值不存在的索引时才会触发。如果上例中date改成name,则不会有“Successfully set….”语句输出。
自己运行代码试一试,是不是发现最后print输出的语句有些不对?是的,因为在__newindex中,我们只是规定了一个输出语句,并没有做真正的赋值操作!所以按照常规思路,就这么改吧:
--错的,请勿模仿!
local mt = {
__newindex = function(t, k, v)
print("Successfully set element \"" .. tostring(k) ..
"\" as " .. tostring(v))
t[k] = v
end
}
不要高兴太早,其实。。并没有这么简单。博主尝试过t[k] = v和return t[k]和return v等等方法,无一例外都失败了。具体原因说起来可能有点复杂,大家不妨先继续往下。
虽然我们暂时不清楚不知道上面的为什么错,但我们还是知道应该怎么做对的。既然index和newindex都是对空索引触发,那么我们可以利用一个代理的思想,来解决问题。直接贴书上给的例子:
t = {}
local _t = t --创建代理
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("Successfully set element \"" .. tostring(k) ..
"\" as " .. tostring(v))
_t[k] = v
end
}
setmetatable(t, mt)
t[2] = "hello"
print(t[2])
得到的输出如下:
Successfully set element "2" as hello
*access to element 2
hello
这下,上面的问题终于解决了。不过请注意,index和newindex最后的返回值都是针对_t[k]操作的,一切的赋值最后都给了_t[k],而所有对t的访问实际上都是对_t[k]的访问。这也是“代理”的意义所在,不管怎么折腾,最后变的是_t,而t永远都是个空table,说白了t就是个傀儡(细思极恐)。
这下,你是不是可以对上例的这个错误进行解释了呢:
--错的,请勿模仿!
local mt = {
__newindex = function(t, k, v)
print("Successfully set element \"" .. tostring(k) ..
"\" as " .. tostring(v))
t[k] = v
end
}
我就不在这里展开说了,大家不妨自己想想(提示:就算在__newindex内部,__newindex还是会被触发的)。
4. __index和__newindex组合技
把__index和__newindex两个神器结合起来,就可以做出很多有趣的东西,上面的追踪访问可以算是一例。
只读table
只要稍微修改上例的newindex语句,就可以创建出一个只读的table:
t = {
name = "Jiang",
date = "1926.8"
}
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("table t is read-only!!")
end
}
setmetatable(t, mt)
t["date"] = "1926.9"
t["sex"] = "male"
print(t["date"])
print(t["sex"])
所有试图修改t的操作都被禁止了:因为t是一个空变量,所以对t的写操作必定会触发__newindex。
通过一个table给另一个table赋值
这个例子直接转载另一篇博客上的代码(http://www.jb51.net/article/55155.htm)(由于我的电脑终端不支持中文,所以改成了全英文)
local smartMan = {
name = "none"
}
local other = {
name = "hello, I'm innocent table"
}
local t1 = {}
local mt = {
__index = smartMan,
__newindex = other
}
setmetatable(t1, mt)
print("other's name(before): " .. other.name)
t1.name = "thief"
print("other's name(after): " .. other.name)
print("t1's name: " .. t1.name)
输出的结果:
other's name(before): hello, I'm innocent table
other's name(after): thief
t1's name: none
我们试图给t1赋值,由于t1本来是空的,所以根据__newindex的定义转到了other表格,实际上被赋值的是other,t1依然是空变量。而最后一句试图访问t1.name,根据__index则转到了smartMan这个变量,打印的值实际是smartMan。
同时监视几个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 = track(t)