# 前言
Lua 並不是為了物件導向(Object-Oriented)而設計的一種語言,因此僅從原生語法上並不直接支持物件導向的編程,但依然可以透過一些自身的機制實現類似的效果
>[!Tip]物件導向幾大特性:
>封裝 Encapsulation
>繼承 Inheritance
>多型 Polymorphism
# Lua 基本語法特性
## 函數的宣告和調用
Lua 在函數的宣告和調用時,有點號(‧)和冒號(:)兩種寫法,點號是一般預設的用法
```gherkin=
local human = {}
function human.say(content)
print(content)
end
human.say("Hello")
```
```gherkin=
Hello
```
---
冒號之於點號是一種語法糖關係,默認會有個第一排序的隱藏參數 self,指該對象本身
```gherkin=
local human = {}
function human:say(content)
print(content)
end
human:say("Hello")
```
```gherkin=
Hello
```
等同於
```gherkin=
local human = {}
function human.say(self, content)
print(content)
end
human.say(human, "Hello")
```
```gherkin=
Hello
```
---
兩者可以混用,但一旦使用了冒號,該函數就會多個第一排序的隱藏參數,以下有幾個錯誤例子
```gherkin=
local human = {}
function human.say(content)
print(content)
end
human:say("Hello")
```
```gherkin=
table: 0x556e9a12cf40
```
```gherkin=
local human = {}
function human:say(self, content)
print(content)
end
human.say("Hello")
```
```gherkin=
nil
```
正確的混用例子
```gherkin=
local human = {}
function human.say(self, content)
print(content)
end
human:say("Hello")
```
```gherkin=
Hello
```
## 可變參數 arg
Lua 的函數可以接受不固定數量的參數,在函數參數列表中使用 ... 來表示
```gherkin=
local human = {}
function human:say(content, ...)
local arg = { ... }
print(content)
local argContent = ""
for i, v in ipairs(arg) do
argContent = argContent .. v
end
print(argContent)
end
human:say("Hello", "I'm ", "Human", ".")
```
```gherkin=
Hello
I'm Human.
```
## 元表 metatable
元表是一個對於表(table)的特殊屬性,元表可以設定或改變該表的部分行為
```gherkin=
local human = {} -- 元表
local man = {} -- 表
setmetatable(man, human) -- 將 human 設為 man 的元表
local mt = getmetatable(man) -- 返回 man 的元表,即 human
```
在元表中,有許多元方法(Metamethods)可以使用,其中以 ˍˍindex 為本文重點,其作用為當表被訪問時,若該表不存在指定的 key 值,則會往元表中搜尋
其他還有諸如 ˍˍadd、ˍˍsub、ˍˍeq、ˍˍtostring 等等元方法,就先不展開細講了
# 繼承
在 C++ 或 python 等這樣的 OO 語言中,使用 class 語言來創建 object、instance,即每個對象都是特定類別的實例
而在 Lua 中沒有類的概念,也就是說對象是沒有類型的,若要創建多個具有類似行為的對象,則需要透過元表的 ˍˍindex 元方法
將子類 table 的 ˍˍindex 元方法設置為父類 table,如此一來子類被調用自身沒有的屬性或函數時,就會去查找元表,實現類似繼承的效果
```gherkin=
local human = { name = "Human" }
function human:say()
print("Hello, I'm " .. self.name .. ".")
end
local man = {}
human.__index = human
setmetatable(man, human)
man:say()
```
```gherkin=
Hello, I'm Human.
```
其中上述第 6-8 行可以簡化寫法
```gherkin=
local man = setmetatable({}, { __index = human })
```
:::info
另外提一個我在 VSCode 開發中的現象,若是不使用簡化寫法,在查找子類所繼承的屬性或函數的實作時,使用移至定義 ( Go to Definition ) 是找不到實作位置的;使用簡化寫法就沒問題,不確定原理,不清楚其他編輯器是怎麼樣
:::
---
甚至可以模仿建構函數的方式來創建對象,其中建構函數 ctor 的命名是自訂的,Lua 並沒有提供建構的固定保留字,想用 new 之類的都可以
```gherkin=
local human = { name = "Human" }
function human:ctor()
function human:say()
print("Hello, I'm " .. self.name .. ".")
end
local o = setmetatable({}, { __index = self })
return o
end
local man = human:ctor()
man:say()
```
```gherkin=
Hello, I'm Human.
```
建構函數內要帶值也沒問題
```gherkin=
local human = {}
function human:ctor(c)
self.name = c or "Human"
function human:say()
print("Hello, I'm " .. self.name .. ".")
end
local o = setmetatable({}, { __index = self })
return o
end
local man = human:ctor("Adam")
man:say()
```
```gherkin=
Hello, I'm Adam.
```
---
但此時若子類有複數個,就會出現以下問題
```gherkin=
local human = {}
function human:ctor(c)
self.name = c or "Human"
function human:say()
print("Hello, I'm " .. self.name .. ".")
end
local o = setmetatable({}, { __index = self })
return o
end
local man = human:ctor("Adam")
local woman = human:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Eve.
Hello, I'm Eve.
```
原因是在第 3 行,因為 self 指的就是 human 本身,所以在第 13、14 行建構時,因為子類 man 沒有 name 的屬性,就會根據元表查到父類 human 的 name 進行賦值
而複數的子類依序對單一的父類進行賦值,才會導致最終所有子類的屬性的值都是一樣的,因為說穿了就是都導向了同樣的對象
解決辦法是更改賦值的對象,在 ctor 時提前宣告對象,並使屬性、函數的對象都指向它
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.name = c or "Human"
function o:say()
print("Hello, I'm " .. self.name .. ".")
end
return o
end
local man = human:ctor("Adam")
local woman = human:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Eve.
```
另解,也可以在宣告時同時賦予子類相同的屬性,函數也同理
```gherkin=
local human = {}
function human:ctor(c)
self.name = c or "Human"
function human:say()
print("Hello, I'm " .. self.name .. ".")
end
local o = setmetatable({ name = self.name }, { __index = self })
return o
end
local man = human:ctor("Adam")
local woman = human:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Eve.
```
# 多型
除了單純繼承外,另外重要的概念還有派生,即允許由父類衍生子類,且除了繼承基底類別的行為,子類還可以定義自己獨有的行為,即多型
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.name = c or "Human"
function o:say()
print("Hello, I'm " .. self.name .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
o.say = function(self)
print("Hello, I'm " .. self.name .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve, nice to meet you.
```
甚至可以直接宣告同名屬性或函數,原理也很簡單,外部在調用子類的屬性或函數時,直接在子類內找到同名,就不會透過元表查找父類了,實現類似覆寫的效果
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.name = c or "Human"
function o:say()
print("Hello, I'm " .. self.name .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
function o:say()
print("Hello, I'm " .. self.name .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve, nice to meet you.
```
---
若是不想完全覆寫,或是仍想調用父類的屬性或函數,可以在子類利用 getmetatable 來實現
其中 super 的命名是自訂的,Lua 並沒有調用父類的固定保留字,想用 sup 之類的都可以
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.name = c or "Human"
function o:say()
print("Hello, I'm " .. self.name .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
o.super = setmetatable({}, getmetatable(o))
function o:say()
self.super:say()
print("Hello, I'm " .. self.name .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```
或是在父類直接定義好
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.super = setmetatable({}, { __index = o })
o.name = c or "Human"
function o:say()
print("Hello, I'm " .. self.name .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
function o:say()
self.super:say()
print("Hello, I'm " .. self.name .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```
# 封裝
在設計物件導向的程式時,都會希望那些不需要對外公開的屬性或函數進行封裝,避免外部亂調用或賦值,同時也方便開發排查
而一般在 Lua 裡屬性和函數都是預設全域的,可以被外部調用或賦值
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.super = setmetatable({}, { __index = o })
o.name = c or "Human"
function o:say()
print("Hello, I'm " .. self.name .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
function o:say()
self.super:say()
print("Hello, I'm " .. self.name .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man.name = "Lilith"
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```
---
上述 human 的屬性 name 為全域屬性,可以被外部賦值,且僅對子類進行賦值,父類的 super 並未被賦值
這並非預想的情況,所以需要使用 local 區域化,並提供對外讀寫函數
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.super = setmetatable({}, { __index = o })
local name = c or "Human"
function o:getName()
return name
end
function o:setName(value)
name = value
end
function o:say()
print("Hello, I'm " .. self:getName() .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
function o:say()
self.super:say()
print("Hello, I'm " .. self:getName() .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:setName("Lilith")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Lilith.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```
同理另解,也可以在宣告時同時賦予子類相同的函數
```gherkin=
local human = {}
function human:ctor(c)
local name = c or "Human"
function human:getName()
return name
end
function human:setName(value)
name = value
end
function human:say()
print("Hello, I'm " .. self:getName() .. ".")
end
local o = setmetatable({
getName = self.getName,
setName = self.setName,
say = self.say
}, { __index = self })
o.super = setmetatable({
getName = self.getName,
setName = self.setName,
say = self.say
}, { __index = o })
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
function o:say()
self.super:say()
print("Hello, I'm " .. self:getName() .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:setName("Lilith")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Lilith.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```
# 額外補充
## 多載
Lua 無法像 C++ 或 Java 一樣支援多載,會出現「後覆蓋前」的情況
```gherkin=
local human = {}
function human:ctor()
local o = setmetatable({}, { __index = self })
function o:say()
print("Hello, I'm human.")
end
function o:say(name)
name = name or ""
print("Hello, I'm " .. name .. ".")
end
return o
end
local man = human:ctor()
man:say()
man:say("Adam")
```
```gherkin=
Hello, I'm .
Hello, I'm Adam.
```
改用可變參數來處理可以達到同樣效果
```gherkin=
local human = {}
function human:ctor()
local o = setmetatable({}, { __index = self })
function o:say(...)
local arg = { ... }
name = arg[1] or ""
if name == "" then
print("Hello, I'm human.")
else
print("Hello, I'm " .. name .. ".")
end
end
return o
end
local man = human:ctor()
man:say()
man:say("Adam")
```
```gherkin=
Hello, I'm human.
Hello, I'm Adam.
```
# 結語
Lua 確實可以透過一些方法和自身的機制實現物件導向設計類似的效果,雖然需要一些學習成本,也要習慣比較特殊的寫法,不過對於需要使用物件導向來開發的開發者來說,也算是個比較折衷的辦法了,~~不然就是換語言~~
最後統整起來的腳本:
```gherkin=
local human = {}
function human:ctor(c)
local o = setmetatable({}, { __index = self })
o.super = setmetatable({}, { __index = o })
local name = c or "Human"
function o:getName()
return name
end
function o:setName(value)
name = value
end
function o:say()
print("Hello, I'm " .. self:getName() .. ".")
end
return o
end
local humanBeing = {}
function humanBeing:ctor(c)
local base = human:ctor(c)
local o = setmetatable({}, { __index = base })
function o:say()
self.super:say()
print("Hello, I'm " .. self:getName() .. ", nice to meet you.")
end
return o
end
local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:setName("Lilith")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Lilith.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```