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