【Lua 筆記】元表(MetaTable) - part 10 === 目錄(Table of Contents): [TOC] --- 由於有款遊戲叫做 CSO(Counter-Strike Online),內建模式創世者模式(Studio)新增使用 Lua 及其遊戲的 API,所以突發奇想製作這個筆記。 這個筆記會在一開始先著重純粹的程式設計自學,在最後的章節才會與 CSO 遊戲 API 進行應用。 元表(MetaTable) --- > 在 Lua table 中我們可以存取到對應的 key 來得到 value 值,但是卻無法對兩個 table 進行運算操作(例如相加)。因此 Lua 提供了元表(MetaTable),允許我們改變 table 的行為,每個行為關聯了對應的元方法。 所以元表(MetaTable)可以讓我們針對 table 進行一些運算操作。 而在對 table 跟 table 之間進行運算的時候,Lua 首先會檢查兩者之間是否存在元表這個東西,例如會檢查是否有 `__add` 存在,找到的話,則其對應的值(往往是一個函數或是 table)就是"元方法"。 有兩個很重要的函數是專門用來處理元表的: * setmetatable(table, metatable):對指定 table 設定元表(metatable),如果元表(metatable)中存在 `__metatable` 鍵值,setmetatable 會失敗。 * getmetatable(table):回傳物件的元表(metatable)。 關於第一個最後面敘述講到為什麼會失敗,在這邊稍微解釋一下: 當我們用 setmetatable 為一個表設定元表時,如果提供的元表中包含 `__metatable` 鍵,Lua 會阻止這次操作並回傳錯誤。***目的是保護元表不被隨意修改***。(類似於不可變物件:immutable object) 以下是一個範例,有關於設置元表: ```lua= mytable = {} -- table mymetatable = {} -- metatable setmetatable(mytable, mymetatable) -- 把 mymetatable 設為 mytable 的元表 ``` 可直接寫成一行: ```lua= mytable = setmetatable({},{}) ``` ### `__index` 元方法 --- `__index` 是最常用的鍵。 > 當你透過鍵來存取 table 的時候,如果這個鍵沒有值,那麼 Lua 就會尋找該 table 的metatable(假設有 metatable)中的 `__index` 鍵。如果 `__index` 包含一個表,Lua會在 table 中尋找對應的鍵。 什麼意思?如果該鍵在 table 中不存在(即沒有對應的值),Lua 不會立即回傳 nil,而是會檢查這個 table 是否有一個元表(metatable)。如果有,Lua 會在這個元表中尋找一個名為 `__index` 的鍵。 以下是來自菜鳥教程的範例(備註:以下範例是在 Shell 當中進行的,請注意): ```lua $ lua Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio > other = { foo = 3 } > t = setmetatable({}, { __index = other }) > t.foo 3 > t.bar nil ``` 以下是筆者自創範例: ```lua= local myTable = {name = "Programming Bot"} -- 定義一個元表,其中包含 __index 鍵 local mt = { __index = function(table, key) -- 當存取不存在的鍵時,會呼叫這個函數 if key == "age" then -- 假設我們知道 Programming Bot 的年齡,但沒有直接在 table 中定義 return 10 else -- 其他不存在的鍵,回傳 nil return nil end end } -- 設定 myTable 的元表為 mt setmetatable(myTable, mt) -- 嘗試存取存在的鍵 print("Name: " .. myTable.name) -- Name: Programming Bot -- 嘗試存取不存在的鍵,但是由 __index 處理 print("Age: " .. myTable.age) -- Age: 10 -- 嘗試存取完全不存在的鍵,且 __index 中也沒有處理 print("Location: " .. (myTable.location or "unknown")) -- Location: unknown ``` :::info Line 27 : print("Location: " .. (myTable.location or "unknown")) -- Location: unknown: (myTable.location or "unknown"):在 Lua 中,or 運算子會回傳左側運算元的值,如果左側運算元的值為 false 或 nil,則回傳右側運算元的值。所以如果 myTable.location 為 nil(即 location 鍵不存在或其值為 nil),則運算式的結果為 "unknown"。 ::: 以下總結取自[Lua 元表(Metatable) | 菜鸟教程](https://www.runoob.com/lua/lua-metatables.html),老實講寫的很好: Lua 找出一個表中元素時的規則,其實就是如下 3 個步驟: 1. 在表中查找,如果找到,回傳該元素,找不到則繼續。 2. 判斷該表是否有元表,如果沒有元表,回傳 nil,有元表則繼續。 3. 判斷元表有沒有 `__index` 方法,如果 `__index` 方法為 nil,則回傳 nil;如果 `__index` 方法是一個表,則重複 1、2、3 步驟;如果__index 方法是一個函數,則回傳該函數的回傳值。 ### `__newindex` 元方法 --- `__newindex` 元方法用來對表進行更新,`__index` 則用來對表進行存取 。 > 當你給表的一個缺少的索引賦值,解釋器就會找 `__newindex` 元方法:如果存在則呼叫這個函數而不進行賦值操作。 以下是一個範例: ```lua= local myTable = {} local mt = { -- __newindex元方法用於處理對缺少的索引的更新 __newindex = function(table, key, value) print("嘗試更新不存在的鍵:" .. key .. ",將其設定為:" .. tostring(value)) -- 如果要實際進行賦值,可以這樣做: rawset(table, key, value) -- 使用rawset來避免再次觸發__newindex end } setmetatable(myTable, mt) myTable.someKey = "someKey" -- 觸發 __newindex 元方法 -- 存取剛剛更新的鍵 print(myTable.someKey) -- someKey ``` 輸出結果: ``` 嘗試更新不存在的鍵:someKey,將其設定為:someKey someKey ``` :::info rawset(table, key, value): * table:要修改的表。 * key:要修改的鍵(索引)。 * value:要設定的值。 rawset 是 Lua 中的一個函數,它用於直接對 table 進行賦值運算,不觸發任何元方法(如__newindex)。這函數的作用是在特定情況下,允許我們直接繞過 Lua 的元方法機制,直接修改 table 的內容。 ::: ### 運算型元方法 --- 表格來源 [Lua 元表(Metatable) | 菜鸟教程](https://www.runoob.com/lua/lua-metatables.html): | 鍵 | 描述 | | -------- | -------- | | `__add` | 對應運算子"+" | | `__sub` | 對應運算子"-"(做二元減法運算,兩數相減) | | `__mul` | 對應運算子"*" | | `__div` | 對應運算子"/" | | `__mod` | 對應運算子"%" | | `__unm` | 對應運算子"-"(做一元減法運算,當作負號) | | `__concat` | 對應運算子 '..'| | `__eq` | 對應運算子"==" | | `__lt` | 對應運算子"<" | | `__le` | 對應運算子"<=" | 以下是個範例: ```lua= function table_maxn(t) local mn = 0 for k, v in pairs(t) do if mn < k then mn = k end end return mn end mytable = setmetatable({ 1, 2, 3 }, { __add = function(mytable, newtable) for i = 1, table_maxn(newtable) do table.insert(mytable, table_maxn(mytable) + 1, newtable[i]) end return mytable end, __sub = function(mytable, newtable) for i = 1, table_maxn(newtable) do for j = 1, table_maxn(mytable) do if mytable[j] == newtable[i] then table.remove(mytable, j) break end end end return mytable end, __mul = function(mytable, newtable) local result = {} for i = 1, table_maxn(mytable) do for j = 1, table_maxn(newtable) do table.insert(result, mytable[i] * newtable[j]) end end return result end, __div = function(mytable, newtable) local result = {} for i = 1, table_maxn(mytable) do for j = 1, table_maxn(newtable) do table.insert(result, mytable[i] / newtable[j]) end end return result end, __mod = function(mytable, newtable) local result = {} for i = 1, table_maxn(mytable) do for j = 1, table_maxn(newtable) do table.insert(result, mytable[i] % newtable[j]) end end return result end, __unm = function(mytable) local result = {} for i = 1, table_maxn(mytable) do table.insert(result, -mytable[i]) end return result end }) print("mytable = mytable + secondtable") secondtable = { 4, 5, 6 } mytable = mytable + secondtable for k, v in ipairs(mytable) do print(k, v) end print("--------------") print("mytable = mytable - secondtable") thirdtable = { 2, 4 } mytable = mytable - thirdtable for k, v in ipairs(mytable) do print(k, v) end print("--------------") print("mytable * secondtable") result = mytable * secondtable for k, v in ipairs(result) do print(k, v) end print("--------------") print("mytable / secondtable") result = mytable / secondtable for k, v in ipairs(result) do print(k, v) end print("--------------") print("mytable % secondtable") result = mytable % secondtable for k, v in ipairs(result) do print(k, v) end print("--------------") print("-mytable") result = -mytable for k, v in ipairs(result) do print(k, v) end ``` ### `__call` 元方法 --- `__call` 元方法:允許定義一個 table 在被當作函數呼叫時的行為。表示能讓一個 table 像函數一樣被呼叫,並且可以自訂在被呼叫時應該做什麼。(總之就是讓表變成函數那般作用) 以下是一個範例(來自菜鳥教程): ```lua= function table_maxn(t) local mn = 0 for k, v in pairs(t) do if mn < k then mn = k end end return mn end mytable = setmetatable({ 10 }, { __call = function(mytable, newtable) sum = 0 for i = 1, table_maxn(mytable) do sum = sum + mytable[i] end for i = 1, table_maxn(newtable) do sum = sum + newtable[i] end return sum end }) newtable = { 10, 20, 30 } print(mytable(newtable)) -- 把表當作函數呼叫 ``` 輸出結果: ``` 70 ``` 這支程式碼主要是用作於兩表之間的值進行相加。 ### `__tostring` 元方法 --- `__tostring` 元方法:用於定義當一個 table 被轉換為字串時的行為。總之就是修改 table 的輸出行為,`__tostring` 必定回傳字串。 以下是一個範例(來自菜鳥教程): ```lua= mytable = setmetatable({ 10, 20, 30 }, { __tostring = function(mytable) sum = 0 for k, v in pairs(mytable) do sum = sum + v end return "表所有的元素和為 " .. sum end }) print(mytable) ``` 輸出結果: ``` 表所有的元素和為 60 ``` 總結 --- 元表(MetaTable)可以讓我們針對 table 進行一些運算操作。 而在對 table 跟 table 之間進行運算的時候,Lua 首先會檢查兩者之間是否存在元表這個東西,例如會檢查是否有 `__add` 存在,找到的話,則其對應的值(往往是一個函數或是 table)就是"元方法"。 有兩個很重要的函數是專門用來處理元表的: * setmetatable(table, metatable):對指定 table 設定元表(metatable),如果元表(metatable)中存在 `__metatable` 鍵值,setmetatable 會失敗。 * getmetatable(table):回傳物件的元表(metatable)。 關於第一個最後面敘述講到為什麼會失敗,在這邊稍微解釋一下: 當我們用 setmetatable 為一個表設定元表時,如果提供的元表中包含 `__metatable` 鍵,Lua 會阻止這次操作並回傳錯誤。***目的是保護元表不被隨意修改***。(類似於不可變物件:immutable object) `__index` 是最常用的鍵。主要功用是用於 table 的查找(存取 table)跟指定預設值。 `__newindex` 用於對 table 進行更新。 ### 運算型元方法 | 鍵 | 描述 | | -------- | -------- | | `__add` | 對應運算子"+" | | `__sub` | 對應運算子"-"(做二元減法運算,兩數相減) | | `__mul` | 對應運算子"*" | | `__div` | 對應運算子"/" | | `__mod` | 對應運算子"%" | | `__unm` | 對應運算子"-"(做一元減法運算,當作負號) | | `__concat` | 對應運算子 '..'| | `__eq` | 對應運算子"==" | | `__lt` | 對應運算子"<" | | `__le` | 對應運算子"<=" | `__call` 將表變成函數使用。 `__tostring` 回傳值必須是字串。 參考資料 --- [【30天Lua重拾筆記28】進階議題: Meta Programming | 又LAG隨性筆記](https://www.lagagain.com/post/30%E5%A4%A9lua%E9%87%8D%E6%8B%BE%E7%AD%86%E8%A8%9828meta-programming/) [元表與元方法 (Metatables and metamethods) · Lua 基礎](https://keviner2004.gitbooks.io/lua-basic/content/metatables-metamethods/) [Lua - Metatables](https://www.tutorialspoint.com/lua/lua_metatables.htm) [Lua 元表(Metatable) | 菜鸟教程](https://www.runoob.com/lua/lua-metatables.html)