Lua 支持虛函數的解決方案

概述

lua本身沒有提供類似C++虛函數機制,調用的父類方法調用虛函數可能會出現問題。

問題分析

分析這段代碼和輸出

local Gun = {}
-- 示例,實際應用還要考慮構造,虛表等情況
function LuaClass(Class, Parent)
    setmetatable(Class, {__index = Parent})
    Class._Super = Parent
end

function Gun:Attack()
 print("開始攻擊");
 self:Load()
 self:Fire()
end

function Gun:Load()
 print("裝彈");
end

function Gun:Fire()
 print("開槍");
end

Gun:Attack();

local Cannon = {}
LuaClass(Cannon, Gun)

function Cannon:Attack()
    print("大炮開始攻擊")
    self._Super:Attack()
end

function Cannon:Fire()
 print("開炮")
end
print("-------------------------------------")
Cannon:Attack()

輸出:
image
紅線圈出的地方虛函數調用錯誤,應該打印”開炮”。
使用元表來面向對象時,要注意__index元方法的語義:

當你通過鍵來訪問 table 的時候,如果這個鍵沒有值,那麼Lua就會尋找該table的metatable(假定有metatable)中的__index 鍵。如果__index包含一個表格,Lua會在表格中查找相應的鍵
如果__index包含一個函數的話,Lua就會調用那個函數,table和鍵會作為參數傳遞給函數。
__index 元方法查看錶中元素是否存在,如果不存在,返回結果為 nil;如果存在則由 __index 返回結果

可知__index只是提供一種遞歸的查詢方式,其中並未包含虛函數的調用機制。


Gun:Attack() 等價於 Gun.Attack(self)
self._Super:Attack() 等價於 Gun.(Gun) 注意self._Super = Gun
所以調用父類Attack函數中,self的語義是Gun這張表,後面調用的就一直是Gun方法,所以最好調用的是Gun的Fire,而不是Cannon的Fire。

解決方案

使用指針指向調用函數的表,在調用父類的方法時,使父類的self的語義是調用者。
注意這種實現和C++的虛函數調用思路是不一樣的,細節請參考我的另一篇文章:
跳轉鏈接:c++虛函數表、多態

替換問題分析中的LuaClass方法

function LuaClass(Class, Parent)
    local FindVal = function(InClass, Key)
        local Raw = rawget(InClass, Key)
        if nil ~= Raw then
            return Raw, InClass
        end
        if nil ~= InClass.__Base then
            return FindVal(InClass.__Base, Key)
        end
    end
    
    Class.__Base = Parent
    Class.__ClassPtr = Class
    
    local Index = function(_, Key)
        local Val, ClassPtr = FindVal(Parent, Key)
        if nil == Val then
            return
        end
        
        Class.__ClassPtr = ClassPtr
        return Val
    end
    
    setmetatable(Class, {__index = Index})
    
    local SuperIndex = function(_, Key)
        return function(_, ...)
            local OriClassPtr = Class.__ClassPtr
            if nil == OriClassPtr.__Base then
                return
            end
            local Val, ClassPtr = FindVal(OriClassPtr.__Base, Key)
            if nil == Val then
                return
            end
            Class.__ClassPtr = ClassPtr
            local Ret = {Val(Class, ...)}
            Class.__ClassPtr = OriClassPtr
            return table.unpack(Ret)
        end
    end

    Class._Super = setmetatable({}, {__index = SuperIndex})
end

輸出:
image

  • 在__index元方法查詢的時候,標記當前調用方法所在的表。
  • 在_Super的元表__index元方法查詢的時候,找到標記表的方法,使用Class表作為第一個參數self傳入。

備註

  • 支持虛函數有性能開銷,可以在LuaClass加個參數控制是否支持虛函數。
Tags: