【在主画面加入捷径】
       
【选择语系】
繁中 简中

Lua 程序设计教学:多态 (Polymorphism)

【赞助商连结】

    由于 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 是动态类型语言,不需泛型即可自动将同一套程序代码套用在不同类型的参数中。