快速掌握Lua 5.3 —— "metatables" and "metamethods" (2)


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

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com 联系我们: