前言
由於 Lua 是動態型別語言,不需要像 Java 等語言,利用子型別 (subtyping) 來達到多型的效果,使用內建的語法機制即可達到相同的效果。
在本文中,我們從 Lua 的角度來看一些和多型相關的議題。
Duck Type
像 Lua 等動態型別語言較為靈活,程式設計者不需要在意物件的型別 (type),只需符合公開的方法即可。如下例:
local Duck = {}
Duck.__index = Duck
function Duck:new()
self = {}
setmetatable(self, Duck)
return self
end
function Duck:speak()
print("Pack pack")
end
local Dog = {}
Dog.__index = Dog
function Dog:new()
self = {}
setmetatable(self, Dog)
return self
end
function Dog:speak()
print("Wow wow")
end
local Tiger = {}
Tiger.__index = Tiger
function Tiger:new()
self = {}
setmetatable(self, Tiger)
return self
end
function Tiger:speak()
print("Halum halum")
end
do
local animals = {
Duck:new(),
Dog:new(),
Tiger:new()
}
for _, a in ipairs(animals) do
a:speak()
end
end
由於動態型別語言本身的性質,在本例中,只要物件滿足 speak
方法的實作,就可放入 animals
陣列中,不需要用繼承或介面去實作型別樹,也不需要檢查實際的型別。
模擬函式重載
Lua 本身不支援函式重載 (function overloading),不過,由於 Lua 是動態型別語言,可以用一些 dirty hacks 去模擬。如以下實例用執行期的型別檢查來模擬函式重載:
local function add(a, b)
if type(a) == "string" or type(b) == "string" then
return a .. b
else
return a + b
end
end
-- The main program.
do
local p = add("1", 2)
local q = add(1, 2)
assert(p == "12")
assert(q == 3)
end
除了上述方法外,由於 Lua 支援任意長度參數,只要在函式內根據不同的參數給予相對應的行為,就可以模擬函式重載。然而,過度使用此特性,會使得程式難以維護,實務上不鼓勵這種方法。
Lua users wiki 上提供一個較複雜的方法 (見此處),因程式碼較長,這裡不重覆貼出。由於此方式較為複雜,筆者本身未採用此種方法來模擬函式重載,讀者可自行評估是否符合自身的需求。
也可以直接將表 (table) 做為參數傳入函式,對於未賦值的參數直接給予預設值即可。如下例:
local function foo(args)
local args = args or {}
local _args = {}
_args.a = args.a or 1
_args.b = args.b or 2
_args.c = args.c or 3
return _args.a + _args.b + _args.c
end
The main program.
do
assert(foo() == 6)
assert(foo({a = 3}) == 8)
assert(foo({a = 3, b = 4}) == 10)
assert(foo({a = 3, b = 4, c = 5}) == 12)
end
使用表做為參數,不需要寫死參數的位置,可維護性會比較好一些。
過度地使用函式重載,會使程式可維護性變差,即使我們可以用一些 hack 來模擬,還是要審慎使用。
實踐運算子重載
Lua 透過 metamethod 來達到運算子重載的效果。筆者以數學上的向量 (vector) 來展示其實作法:
local Vector = {}
Vector.__index = Vector
-- Implement __eq for equality check.
Vector.__eq = function (a, b)
if a:len() ~= b:len() then
return false
end
for i = 1, a:len() do
if a:at(i) ~= b:at(i) then
return false
end
end
return true
end
-- Implement __add for vector addition.
Vector.__add = function (a, b)
assert(a:len() == b:len())
local out = Vector:new(a:len())
for i = 1, a:len() do
out:setAt(i, a:at(i) + b:at(i))
end
return out
end
-- Create a new vector with specific size.
function Vector:new(size)
assert(size > 0)
self = {}
self._vec = {}
for i = 1, size do
table.insert(self._vec, 0)
end
setmetatable(self, Vector)
return self
end
-- Create a vector from a Lua array on-the-fly.
function Vector:fromArray(t)
local out = Vector:new(#t)
for i, v in ipairs(t) do
out:setAt(i, v)
end
return out
end
function Vector:len()
return #(self._vec)
end
function Vector:at(i)
return self._vec[i]
end
function Vector:setAt(i, value)
self._vec[i] = value
end
-- The main program.
do
local p = Vector:fromArray({1, 2, 3})
local q = Vector:fromArray({2, 3, 4})
local v = p + q
assert(v == Vector:fromArray({3, 5, 7}))
end
透過本例,我們可用類似內建數字的符號來操作向量。本例中僅實作 __eq
和 __add
兩個方法,讀者可自行嘗試實作其他的 metamethod。
動態型態語言不需要泛型
由於 Lua 是動態型別語言,不需泛型即可自動將同一套程式碼套用在不同型別的參數中,故不需考慮泛型相關的議題。