# 前言 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. ```