《Programming in Lua 3》讀書筆記(十)

這一部分應該挺重要的,Lua中唯一的數據結構便是table,幾乎所有的的數據操作都是在table的基礎上進行。而本文提到的元表和元方法,便是幫助table實現更強大的功能而設計的。

日期:2014.7.11
Part Ⅱ 
Metatables and Metamethods

Lua中不能直接對table進行相加、比較等操作。除非使用元表(Metatables)。元表可以使得我們改變元素在處理未定義操作的應對行爲,如定義兩個table直接的相加操作。Lua在處理兩個table的相加操作時會首先檢查兩個table是否有元表,且元表中是否有 __add 元方法字段,如果有這個字段則lua會遵循這個字段內定義的操作執行兩個table的相加操作。

Lua中各個類似的變量有一個相關聯的元表?(到底有沒有?),而table與userdata有各自獨立的元表。默認的,新創建的table是沒有元表的:
e.g.
t = {}
print(getmetatable(t))          --nil
此時我們可以通過setmetatable方法來設置元表,元表其實也相當於一個table
e.g.
t1 = {}
setmetatable(t,t1)
print(getmetatable(t) == t1)          --ture

當然,在lua中我們只可以對table執行setmetatable操作,對其餘類型的變量執行這個操作需要使用C 代碼。書上string庫涉及到了給string類型變量執行設置元表的操作。其餘類型的變量默認是沒有元表的?
print(getmetatable("ss"))               -- table
print(getmetatable(10))                  --nil


Arithmetic Metamethods
算術運算元方法

這裏介紹元表的使用,在這裏用一個table表示set,我們需要運算set的並集等操作
e.g.
Set = {} 
local mt = {}          --metatable for sets

function Set.new(l)     --新建一個set,初始化並且設置其元表
     local set = {}     
     setmetatable(set,mt)
     for _ v in ipairs(l) do set[v] = true end
     return set
end

這樣每次我們新建一個set其都會有同樣的元表:
s1 = Set.new{10,20,11,13}
s2 = Set.new{30,1}
print(getmetatable(s1))          --table: 0x7fa1eb4093a0
print(getmetatable(s2))          --table: 0x7fa1eb4093a0


給元表添加元方法:__add 字段表示table如果執行相加操作
mt.__add = Set.union     --注意此時的 __add 字段還是不能使用的,因爲Set.union 還未定義,而且這段代碼也需要放在定義Set.union之後,否則會報錯。正確的用法是先定義再賦值
--假若mt.__add = Set.union
--定義Set.union
function Set.union( a,b )
     local res = Set.new{}
     for k in pairs(a) do
          res[k] = true
     end
     for k in pairs(b) do
          res[k] = true
     end

     return res
end
--此時
s3 = s1 + s2 會報錯     --attempt to perform arithmetic on global 's1' (a table value)
--需要將mt.__add = Set.union 放置在定義Set.union之後。

同樣的,設置 __mul 元方法也是類似的要求
--定義 Set.intersection
function Set.intersection( a,b )
     local res = Set.new{}
     for k in pairs(a) do
          res[k] = b[k]
     end
     return res
end
mt.__mul = Set.intersection

所有的算術運算元方法:
__add(加)、__mul(乘)、__sub(減)、__div(除)、__unm(負)、__mod(取模)、__pow(取冪)、__concat(連接)

Lua在處理兩個變量的算術運算時,針對不同類型的變量,如
e.g.
s = Set.new {1,2,3}
s = s + 8
此時運行的話會報錯:
--bad argument #1 to 'pairs' (table expected, got number) 
其處理兩個變量的算術運算遵循的步驟是:如果第一個變量定義了元方法則使用第一個變量的元方法,而不會再考慮第二個元素的元方法;第一個沒有而第二個有,則使用第二個的;否則就會報錯。
因此爲了更好的控制程序運行,我們需要限制兩個變量爲同類型擁有同一個元表,以__add爲例,可以如下操作:
function Set.union( a,b )
     if getmetatable(a) ~= mt or getmetatable(b) ~= mt then
          error("xxx",2)     --注意這裏的參數是2,表示是提醒用戶是傳遞的參數有問題
     local res = Set.new{}
     for k in pairs(a) do
          res[k] = true
     end
     for k in pairs(b) do
          res[k] = true
     end

     return res
end


Relational Metamethods
關係運算元方法
Lua中的關係運算元方法主要有:
__eq(相等)、__lt(小於)、__le(小於或等於)。而對於其他的關係操作符,Lua直接做了轉換:a ~= b 相當於 not (a == b) 、a > b 相當於 b < a、a >= b 相當於 b <= a;
關係運算元方法的具體使用類似於上文提到的算術運算元方法;
要注意的是,當兩個變量的元方法不同的時候執行相等關係運算會返回false;


Library-Defined Metamethods
庫定義的元方法

__tostring 元方法 
當調用tostring函數的時候,函數首先會尋找變量是否有__tostring 元方法:
同上文:
s = Set.new{1,2,2}
print(s)                                --table: 0x7f8349403d30
print(getmetatable(s))           --table: 0x7f8349409fd0
此時打印出來的並不是其值,但也不是其元表

--to string
function Set.tostring( set )
     local l = {}
     for e in pairs(set) do
          l[#l + 1] = e
     end
     return "{" .. table.concat(l," , ") .. "}"
end

mt.__tostring = Set.tostring
print(s)                                   --{1,2,2}
在設置了其元方法之後,纔會正確的打印出其值

當然我們可以通過一定的方法來保護我們的元表,setmetatabe和getmetatable也是使用到了元方法,我們可以根據這一特性達到我們的目的:
e.g.
mt.__metatable = "cannot change"
s1 = Set.new{}
print(getmetatable(s1))          --cannot change
而當我們想改變其元表的時候
e.g.
setmetatable(s1,mt)               --error:cannot change a protected metatable 
會報錯,不能修改其元表,這樣就達到了我們要保護元表的目的


Table-Access Metamethods
Lua允許通過元表來控制修改和訪問table中不存在的元素的行爲

The __index metamehod
當我們試圖訪問一個table中不存在的元素時,我們得到的值將會是nil。這是一般意義上將的,事實上,我們訪問table中不存在的元素的時候,會觸發編譯器尋找__index 元方法,當沒有該方法的時候會返回nil;而當該元方法存在被定義了,將會返回該方法定義的操作。
這一個特性對我們使用繼承機制,繼承默認變量的時候有很大的幫助,書上也是以這個爲例子做了講解:
--有默認變量的table
prototype = {x = 0,y = 0,width = 100,height = 100}
--元表
mt = {}
--構造函數
function new(o)
     setmetatable(o,mt)
     return o
end
--定義元方法
mt.__index = function(_,key)
     return prototype[key]
end
--創建一個新的table,使用繼承機制,需要技能默認變量的table
w = new{x = 10,y = 20}
print(w.width)           --100
此時w使用到了prototype裏面的值

__index 元方法不一定需要是一個函數,也可以是一個table。當該方法是一個函數的是,Lua會執行函數裏定義的操作,當是一個table的時候,Lua直接在table中執行訪問操作。

函數 rawget(t,i) 可以使得我們訪問table各個元素的時候不去調用 __index 操作.將會對t執行raw訪問?啥意思

The __newindex metamethod
該元方法的作用表現在更新table中元素值的時候。當我們試圖給table中不存在的鍵賦值的時候,編譯器會尋找 __newindex 的元方法,如果有編譯器將會執行該方法定義的操作,否則就會直接賦值。這裏也有一個函數 rawset(t,k,v),該函數會繞開元方法,直接在t中對鍵k設置值v。
書中提到,有效的結合__index 和 __newindex 兩個元方法的使用將會帶來很強大的設計技巧,如創建只讀table,帶默認值的table等。

Tables with default values
table帶有默認的值,其原理主要是在我們訪問一個table中不存在或者沒有賦值的鍵的時候,返回值是一個固定值,這裏就涉及到了 __index 元方法
e.g.
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
以上操作就爲tab設置了默認值0,如果試圖訪問tab中沒有定義或者不存在的元素,返回值將會是0

爲多個不同的table執行設置多個不同默認值的操作
e.g.
local mt = {__index = function (t) return t.___ end}
function setDefault (t,d)
     t.___ = d
     setmetatable(t,mt)
end
這裏的技巧在於,元表的定義在函數的外部,且將默認值存儲在了要設置默認值的table本身的內部。
避免命名衝突的操作:
e.g.
local key = {}          --以一個table作爲key
local mt = {__index = function (t) return t[key] end}
function setDefault (t,d)
     t[key] = d
     setmetatable(t,mt)
end


Tracking table accesses
有效的使用__index 和 __newindex 可以幫助我們監控對table的訪問和賦值操作。結合使用 proxy (代理) 便可以追蹤所有對table的訪問操作並且追蹤到其訪問的值。書上提到的只有當table爲空的時候才能捕獲到所有對其的訪問操作,爲啥?
t = {}     --original table
--keep a private access to the original table
local _t = t
--create proxy 代理
t = {}
--create metatable
local mt = {
     __index = function ( t,k )
          print("*access to element " .. tostring(k))
          return _t[k]
     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"
print(t[2])
打印出來的,追蹤了table從賦值到訪問的過程
*update of element 2 to hello
*access to element 2
hello


Read-only tables
只讀table

只讀table的原理主要就是在試圖給table賦值的時候做限制,這裏就涉及到了__newindex 元方法的使用。
e.g.
--read only table
function readOnly (t)
     local proxy = {}
     local mt = {
          __index = t,
          __newindex = function (t,k,v)
               error("attempt to update a read-only table",2)      --在試圖改變元素的時候拋出錯誤,且參數爲2,表示在報錯的地方將會是調用該方法的地方。
          end
     }
     setmetatable(proxy,mt)
     return proxy
end
通過在__newindex元方法裏面做恰當的修改,便能將我們的table改寫爲只讀table。


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