元表與元方法 (Metatables and metamethods)
元表 (Metatable) 只是普通的 Lua 表格,它包含許多可以覆寫既有操作的方法。舉例來說,你可以自己定義乘法,讓它可以相加兩張表格,這種聽起來很直觀的操作大部份都可以透過元表達成。以下的例子裡會將兩個玩家合併,得到兩者最高分的總和。
local playerOneInfo = { highScore = 10 }
local playerTwoInfo = { highScore = 10 }
local metatable = {
__add = function( table1, table2 )
return {
highScore = table1.highScore + table2.highScore
}
end
}
setmetatable( playerOneInfo, metatable )
local combinedPlayerInfo = playerOneInfo + playerOneInfo
print( combinedPlayerInfo.highScore ) --prints 20
local combinePlayerInfo = playerOneInfo + playerTwoInfo --Error; we never added the metatable to "playerTwoInfo"
以下的操作可以透過定義不同的元法 (metamethod) 被覆寫:
\+
: __add\-
: __sub\*
: __mul/
: __div%
: __mod^
: __pow\-
: __unm — unary operator, i.e. variable = -variable..
: __concat — concatenate strings\#
: __len — length operator==
: __eq — equality operator<
: __lt — less than operator<=
: __le — less than or equal to operator
當 Lua 找不到表格成員的時候,會根據 __index
運算子定義的內容進行後續的處理。如果你嘗試去存取一個在表格內不存在的關鍵 (key),預設會回傳 nil
。藉由 __index
元法 (metamethod),你可以定義當透過關鍵 (key) 存取不到成員時,應該回傳的預設值。
myTable = { x=160, y=80 }
myMetaTable = {
__index = {
width=100,
height=100
}
}
setmetatable( myTable, myMetaTable )
更簡潔的寫法:
myTable = setmetatable(
{
x=160,
y=80
},
{
__index =
{
width=100,
height=100
}
}
)
現在當你存取 myTable.width,你會獲得預設值:100,即使你沒有在該表格設置 width
的值。然而,當你試著存取 myTable.alpha,他會回傳 nil
。因為你沒有在表格內定義他,也沒有在 __index
方法內指定他的預設值。
__index
也可以是一個函式:
myTable = setmetatable( { x=160, y=80 }, {
__index = function( myTable, key )
if key == "width" then
return 100
elseif key == "height" then
return 100
else
return myTable[ key ]
end
end
})
我們透過 __index
定義”取得表格內未定義變數“時的行為,那麼有沒有方法可以定義”設定值“的行為?答案是元方法 (metamethod) __newIndex
,他和 __index
類似,可以設置成表格或函式:
myTable = setmetatable( { x=160, y=80 }, {
__newindex = function( myTable, key, value )
if key == "width" then
rawset( myTable, "height", value * 1.5 )
rawset( myTable, key, value )
elseif key == "height" then
rawset( myTable, "width", value / 1.5 )
rawset( myTable, key, value )
else
rawset( myTable, key, value )
end
end
})
myTable.width = 10
print( myTable.height ) -- prints 15
myTable.height = 20
print( myTable.width ) -- prints 10
等等,為什麼最後一個 print()
不是印出 13.333??? 這是因為 __index
只在設置新的關鍵 (key) 時,才有作用,而 myTable.height
卻已經在你設置 .width
時設置過了 (第 4 行),所以對 __newIndex
來說,它不是一個新的關鍵,也就不會處理了。你可以試著移除設置 .width
的程式碼(第 17 行),就會得到輸出值 13.333。
有一點必須特別注意,如果你沒有透過 rawset()
設置關鍵,程式可能會不斷的執行在元表內 (metatable) 的同一段程式碼,導致無法終止的遞迴 (recursive loop) 發生。使用 rewset()
與 rawget()
設置或存取值不會被元表 (metatable) 所偵測。