2017年1月30日 星期一

Lua 筆記: __index

Lua 對物件導向「繼承」的支援頗類似 JavaScript 的 prototype 繼承,來看一個簡單的例子:

Account = {balance = 0}

Account.__index = Account

function Account:new(o)
    o = o or {}
    setmetatable(o, self)
    return o
end

a1 = Account:new()
print(a1.balance)
執行結果:


Programming in Lua (PIL) 2nd 13.4 指出,當取出 table 不存在的欄位時,interpreter 會進行兩步驟:
  1. 查詢 metatable(上述程式 Line 7)的欄位 __index (稱為 __index meta method) ,如果 __index 不為 nil(上面程式 Line 3),則由 __index 取得最終結果。__index 可以是下列兩種資料型態之一:
    • table: 查詢此 table 的同名欄位(balance)
    • function: 以 table(a1) 與不存在的 key(balance) 呼叫該 function
  2. 如果不存在 metatable 或 metatable 欄位 __index 為 nil,則回傳 nil。
Line 3 把 __index 指向自己(Account)初看有些混亂,既然 a1 (Line 11) 已經設定 metatable(Line7),那何不乾脆直接去 metatable 找就好?不過 Lua meta method 都依循這個規則,詳見 PIL Ch.13。

接下來這個例子可能會讓不少人抓頭老半天:
Account = {balance = 0}

Account.__index = Account

function Account:new(o)
    o = o or {}
    setmetatable(o, self)
    return o
end

a1 = Account:new()

print(Account)
print(Account.__index)
print(a1.__index)
輸出結果:


Line 15 輸出怎麼會跟 Line 13,14 一樣呢?

不過以前述的邏輯推論,沒有任何違和感:
  1. a1.__index 為 nil,於是查詢 metatable (Account) 欄位 __index
  2. metatable 欄位 __index 指向自己,照前面的解釋,如果 __index 是一個 table,則在該 table 查詢對應的欄位,那就是查詢 Account.__index,於是回傳 Account。
如不信,我們刻意把 a1.__index 設為 nil,看看結果如何:
Account = {balance = 0}

Account.__index = Account

function Account:new(o)
    o = o or {}
    setmetatable(o, self)
    return o
end

a1 = Account:new()

print(Account)
print(Account.__index)
a1.__index = nil
print(a1.balance)
輸出結果:


最後,我們以一個例子來欣賞 Lua 的精妙之處:
Account = {balance = 0}

Account.__index = Account

function Account:new(o)
    o = o or {}
    setmetatable(o, self)
    return o
end

function Account:deposit(v)
    self.balance = self.balance + v
end

a1 = Account:new()
a1:deposit(123)
print(a1.balance)
print(Account.balance)
輸出結果:


各位可以看到 a1 有了自己的 balance 欄位,也沒有影響到他的 metatable - Account。

這是怎麼做到的?這裡就要對 Line 12 詳加解釋才行了:

  1. 因為 self.balance == nil,促使 interpreter 尋找 metatable 欄位 __index,於是找到 balance = 0。
  2. self.balance = 0 + v,此時寫回 self 本身(a1),創建新欄位 balance。
這裡要澄清的是當對 table 寫入不存在的欄位時,會喚起的是該 table 的 metatable 欄位 __newindex (meta method),這裡並沒有用到 __newindex,會提到這個的原因是避免有人誤以為讀寫 table 不存在的欄位時都會喚起 metatable __index。(對 __newindex 有興趣的朋友請自行參閱 PIL 13.4)

The End.



1 則留言:

  1. 講的很清楚,很容易理解。
    順便提一下前置知識:訪問一個table的元素時,若該元素不存在,則訪問該table的__index這個meta function,若連__index都不存在,就返回nil
    文章最後提到的寫入不存在的元素時,訪問的則是__newindex這個meta function
    謝謝作者展現了lua的精妙之處!

    回覆刪除