【Lua 筆記】元表(MetaTable) - part 10

由於有款遊戲叫做 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)

以下是一個範例,有關於設置元表:

1
2
3
mytable = {}                          -- table
mymetatable = {} -- metatable
setmetatable(mytable, mymetatable) -- 把 mymetatable 設為 mytable 的元表

可直接寫成一行:

1
mytable = setmetatable({},{})

__index 元方法

__index 是最常用的鍵。

當你透過鍵來存取 table 的時候,如果這個鍵沒有值,那麼 Lua 就會尋找該 table 的metatable(假設有 metatable)中的 __index 鍵。如果 __index 包含一個表,Lua會在 table 中尋找對應的鍵。

什麼意思?如果該鍵在 table 中不存在(即沒有對應的值),Lua 不會立即回傳 nil,而是會檢查這個 table 是否有一個元表(metatable)。如果有,Lua 會在這個元表中尋找一個名為 __index 的鍵。

以下是來自菜鳥教程的範例(備註:以下範例是在 Shell 當中進行的,請注意):

1
2
3
4
5
6
7
8
$ 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

以下是筆者自創範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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) | 菜鸟教程,老實講寫的很好:

Lua 找出一個表中元素時的規則,其實就是如下 3 個步驟:

  1. 在表中查找,如果找到,回傳該元素,找不到則繼續。
  2. 判斷該表是否有元表,如果沒有元表,回傳 nil,有元表則繼續。
  3. 判斷元表有沒有 __index 方法,如果 __index 方法為 nil,則回傳 nil;如果 __index 方法是一個表,則重複 1、2、3 步驟;如果__index 方法是一個函數,則回傳該函數的回傳值。

__newindex 元方法

__newindex 元方法用來對表進行更新,__index 則用來對表進行存取 。

當你給表的一個缺少的索引賦值,解釋器就會找 __newindex 元方法:如果存在則呼叫這個函數而不進行賦值操作。

以下是一個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

輸出結果:

1
2
嘗試更新不存在的鍵:someKey,將其設定為:someKey
someKey

:::info
rawset(table, key, value):

  • table:要修改的表。
  • key:要修改的鍵(索引)。
  • value:要設定的值。

rawset 是 Lua 中的一個函數,它用於直接對 table 進行賦值運算,不觸發任何元方法(如__newindex)。這函數的作用是在特定情況下,允許我們直接繞過 Lua 的元方法機制,直接修改 table 的內容。
:::

運算型元方法

表格來源 Lua 元表(Metatable) | 菜鸟教程

描述
__add對應運算子”+”
__sub對應運算子”-“(做二元減法運算,兩數相減)
__mul對應運算子”*”
__div對應運算子”/“
__mod對應運算子”%”
__unm對應運算子”-“(做一元減法運算,當作負號)
__concat對應運算子 ‘..’
__eq對應運算子”==”
__lt對應運算子”<”
__le對應運算子”<=”

以下是個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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 像函數一樣被呼叫,並且可以自訂在被呼叫時應該做什麼。(總之就是讓表變成函數那般作用)

以下是一個範例(來自菜鳥教程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)) -- 把表當作函數呼叫

輸出結果:

1
70

這支程式碼主要是用作於兩表之間的值進行相加。

__tostring 元方法

__tostring 元方法:用於定義當一個 table 被轉換為字串時的行為。總之就是修改 table 的輸出行為,__tostring 必定回傳字串。

以下是一個範例(來自菜鳥教程):

1
2
3
4
5
6
7
8
9
10
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)

輸出結果:

1
表所有的元素和為 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隨性筆記

元表與元方法 (Metatables and metamethods) · Lua 基礎

Lua - Metatables

Lua 元表(Metatable) | 菜鸟教程