LUA语言中神奇的__index和__newindex

今天有幸学习了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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章