【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)