草庐IT

Lua 面向对象&支持虚函数

寡人正在Coding 2023-03-28 原文

概述

  • 2023-02 据实际开发情况,对原来的方案优化,放在了后面

lua的__index元方法本身没有提供类似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()

输出:

红线圈出的地方虚函数调用错误,应该打印"开炮"。
使用元表来面向对象时,要注意__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

输出:

  • 在__index元方法查询的时候,标记当前调用方法所在的表。
  • 在_Super的元表__index元方法查询的时候,找到标记表的方法,使用Class表作为第一个参数self传入。

备注

  • 支持虚函数有性能开销,可以在LuaClass加个参数控制是否支持虚函数。

更新

  • 2022-02 lua面向对象优化

local function Ctor(Class, Obj, ...)
    if Class._Super ~= nil then
        Ctor(Class._Super, Obj, ...)
    end
    if rawget(Class, "Ctor") then
        Class.Ctor(Obj, ...)
    end
end

local function New(Class, ...)
    local Obj = {}
    setmetatable(Obj, {__index = Class})
    Ctor(Class, Obj, ...)
    Obj._Class = Class
    return Obj
end

local function MakeClass(Parent, SupVirtual)
    local Class = {}

    if Parent == nil or type(Parent) ~= "table" then
        Class.New = New
        return Class
    end

    if SupVirtual then
        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})

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

    Class.New = New
    return Class
end

local function IsA(obj, class)
    if not obj._Class then
        return false
    end

    return obj._Class == class
end

_G.Class = MakeClass
_G.New = New
_G.IsA = IsA

return MakeClass, New

有关Lua 面向对象&支持虚函数的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  3. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  4. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 难道Lua没有和Ruby的method_missing相媲美的东西吗? - 2

    我好像记得Lua有类似Ruby的method_missing的东西。还是我记错了? 最佳答案 表的metatable的__index和__newindex可以用于与Ruby的method_missing相同的效果。 关于ruby-难道Lua没有和Ruby的method_missing相媲美的东西吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7732154/

  7. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  8. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

    为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

  9. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  10. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

随机推荐