Lua 程式設計教學:多型 (Polymorphism)

PUBLISHED ON FEB 18, 2018 — PROGRAMMING

由於 Lua 是動態型別語言,不需要像 Java 等語言,利用子型別 (subtyping) 來達到多型的效果,使用內建的語法機制即可達到相同的效果。

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 是動態型別語言,不需泛型即可自動將同一套程式碼套用在不同型別的參數中。

comments powered by Disqus