---
# System prepended metadata

title: 【Lua 筆記】元表（MetaTable） - part 10
tags: [Lua, 程式語言]

---

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

