元表與元方法 (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) 所偵測。

results matching ""

    No results matching ""