cocos-lua 多重继承的iskindof的BUG

1 篇文章 0 订阅

cocos-lua的iskindof在多重继承时有一个BUG,如下:

    local A=class("ClassA")
    local B=class("ClassB",A)
    local binst= B.new()
    print(   iskindof(binst,"ClassA")  )

ClassB继承自ClassA,因此B的对象理应也是ClassA的对象。但是输出结果却是false. (如果你已经知道这个BUG,想知道怎么解决,直接跳到最后;如果你想看是什么原因,则往下看。)

iskindof源码

那么就要看下iskindof的代码了。源码如下:

    function iskindof(obj, classname)
        local t = type(obj)
        if t ~= "table" and t ~= "userdata" then return false end

        local mt
        if t == "userdata" then
            if tolua.iskindof(obj, classname) then return true end
            mt = tolua.getpeer(obj)
        else       
            mt = getmetatable(obj)   --我传进来的binst是走到这个分支
        end
        if mt then
            --最后调用这个,mt就是binst的metatable,classname就是传进来的"ClassA"
            return iskindof_(mt, classname) 
        end
        return false
    end

略有点复杂,iskindof流程大概是先判断传进来的obj什么类型,如果是table类型的则取它的元表metatable;如果是userdata类型的,则用tolua的方式来判断。

我们的传进去的是binst,是个table类型;所以执行getmetatable。然后再去调用iskindof_。单纯按照kindof这个函数的意图来讲,这个貌似没什么问题:“传进来如果是个table,那么我们就去查它的metatable是个什么类就可以了。”

那再看看最后一步iskindof_是个什么函数,源码如下:

local iskindof_
iskindof_ = function(cls, name)
    local __index = rawget(cls, "__index")
    if type(__index) == "table" and rawget(__index, "__cname") == name then return true end

    if rawget(cls, "__cname") == name then return true end
    local __supers = rawget(cls, "__supers")  --获取的__supers为空
    if not __supers then return false end
    for _, super in ipairs(__supers) do
        if iskindof_(super, name) then return true end
    end
    return false
end

(程序中rawget意思是只找当前表里的内容,避免lua自动去找这个表的元表的内容)

其大致的流程是:
1.先找cls的__index,看看__index的类名是不是我们要的。
2.如果不是,看看cls本身是不是我们要的。
3.如果不是,那么我们看看cls的父类有没有我们要的。找父类的过程是个迭代的过程,只要找到了,则return true;没有找到,则继续找下一个父类。

按照这个函数的意思,如果当前类找不到类名,会去找父类。但是我运行了一下,当把binst的元表传进去之后,__supers为空!这显然是不对的,binst明明是有父类的,怎么会没有__supers呢?肯定哪里错误了。
所以接下来我们要解决的问题是,也是本文的中心:

为什么iskindof\_找不到binst的__supers?

不过,我承认iskindof_这个函数有点不好消化,因为一下子来了那么多新的域,什么__index、__supers、__cname等等。这就涉及到class的内部结构了。
再粘一下class的源码,不要被吓到。吓到了,直接跳过看我的研究成果吧。

class的结构

function class(classname, ...)
    local cls = {__cname = classname}

    local supers = {...}
    for _, super in ipairs(supers) do
        local superType = type(super)
        assert(superType == "nil" or superType == "table" or superType == "function",
            string.format("class() - create class \"%s\" with invalid super class type \"%s\"",
                classname, superType))

        if superType == "function" then
            assert(cls.__create == nil,
                string.format("class() - create class \"%s\" with more than one creating function",
                    classname));
            -- if super is function, set it to __create
            cls.__create = super
        elseif superType == "table" then
            if super[".isclass"] then
                -- super is native class
                assert(cls.__create == nil,
                    string.format("class() - create class \"%s\" with more than one creating function or native class",
                        classname));
                cls.__create = function() return super:create() end
            else
                -- super is pure lua class
                cls.__supers = cls.__supers or {}
                cls.__supers[#cls.__supers + 1] = super
                if not cls.super then
                    -- set first super pure lua class as class.super
                    cls.super = super
                end
            end
        else
            error(string.format("class() - create class \"%s\" with invalid super type",
                        classname), 0)
        end
    end

    cls.__index = cls
    if not cls.__supers or #cls.__supers == 1 then
        setmetatable(cls, {__index = cls.super})
    else
        setmetatable(cls, {__index = function(_, key)
            local supers = cls.__supers
            for i = 1, #supers do
                local super = supers[i]
                if super[key] then return super[key] end
            end
        end})
    end

    if not cls.ctor then
        -- add default constructor
        cls.ctor = function() end
    end
    cls.new = function(...)
        local instance
        if cls.__create then
            instance = cls.__create(...)
        else
            instance = {}
        end
        setmetatableindex(instance, cls)
        instance.class = cls
        instance:ctor(...)
        return instance
    end
    cls.create = function(_, ...)
        return cls.new(...)
    end

    return cls
end

好吧,我承认我自己也被吓到了。其实,其大致思想是先建个local表,再在这个local表添加各种各样的东西(什么__index, __supers, __cname都放进去),接着设置下它的metatable,最后返回这个local表就ok了。人生就是这样,因为考虑得周全了,东西也会变多。

为了解释iskindof,除去所有冗余的判断,我们只挑iskindof需要的域。

    --__cname就是传进来的classname字符串
    local cls = {__cname = classname}

    --因为我们传进来的supers只有一个,所以
    --cls.__supers={YOURSUPERCLASS},
    --cls.super=YOURSUPERCLASS.

    cls.__supers = cls.__supers or {}
    cls.__supers[#cls.__supers + 1] = super
    if not cls.super then
        -- set first super pure lua class as class.super
        cls.super = super
    end

    --cls的__index就是它自己
    cls.__index = cls

    --设置元表
    --如果有supers且只有一个super,则cls的元表很简单就是一个有__index域的表,{__index = YOURSUPERCLASS}
    --如果没有supers或有好多个supers,则__index是个函数。这个函数会去遍历所有的Super;如果没有supers,那么这个__index就相当于一个 一直返回nil的函数。
    if not cls.__supers or #cls.__supers == 1 then
        setmetatable(cls, {__index = cls.super})
    else
        setmetatable(cls, {__index = function(_, key)
            local supers = cls.__supers
            for i = 1, #supers do
                local super = supers[i]
                if super[key] then return super[key] end
            end
        end})
    end

好,有了上面的分析,我们就可以画出我在一开始定义的那两个类的结构啦。再看下我之前定义的两个类:

    local A=class("ClassA")
    local B=class("ClassB",A)
    local binst= B.new()
A
__cname“ClassA”
__supersnil
supernil
__indexA
metatable{ __index=一个总是返回nil的function }

再看看B的结构:

B
__cname“ClassB”
__supers{A}
superA
__indexB
metatable{ __index=A }

根据上面的结构,binst按道理是有__supers的,那iskindof_ 执行下来为啥没有呢?难道binst的结构有问题?
我们就得研究下再来看看binst的结构。

我们的实例——binst的结构

看例子,binst是通过B.new()来创建的。new函数是定义在class下的,代码如下:

    --定义cls的new函数,我们创建类的实例就用这个函数。
    cls.new = function(...)
        local instance
        if cls.__create then
            instance = cls.__create(...)
        else
            instance = {}
        end
        --又多出一个不认识的函数setmetatableindex,汗汗汗,待会儿讲
        setmetatableindex(instance, cls)
        instance.class = cls
        instance:ctor(...)
        return instance
    end

我们的cls是没有__create的,所以一开始instance为{}。然后为这个instance添加域class,并执行ctor函数。这过程还多了个新的函数setmetatableindex,按字面意思就是为instance添加元表。

看看函数的源码吧:

local setmetatableindex_
setmetatableindex_ = function(t, index)
    if type(t) == "userdata" then
        local peer = tolua.getpeer(t)
        if not peer then
            peer = {}
            tolua.setpeer(t, peer)
        end
        setmetatableindex_(peer, index)
    else
        local mt = getmetatable(t)
        if not mt then mt = {} end
        if not mt.__index then
            mt.__index = index
            setmetatable(t, mt)
        elseif mt.__index ~= index then
            setmetatableindex_(mt, index)
        end
    end
end
setmetatableindex = setmetatableindex_

我们传进去instance给t, index为类本身。setmetatableindex先判断t的类型,如果是userdata,则执行userdata那一套;否则执行table的那一套。
我们是table类型,所以先取instance的metatable。由于我们传进去的instance为{},最后会执行下面的两条语句:mt.__index = indexsetmetatable(t,mt)。意思是将instance的metatable设成{__index=index}

综上,得到binst的结构:

binst
classB
metatable{ __index=B }

原来binst也是有metatable:{__index=B}。看上去还蛮简单的嘛。

说了那么多,最后返回来看我们的iskindof到底有什么问题?

再来看iskindof

iskindof的大致流程为:

Created with Raphaël 2.1.0 传入obj, classname obj类型为 table? 取table的 metatable为mt 返回iskindof_(mt,classname) 结束 userdata那一套 yes no

我们传入的obj为binst,classname为”CLassA”。而binst的metatable为{__index=B},意思是iskindof_的传入的是{__index=B}和"ClassB"
隐隐约约感觉到{__index=B}貌似有些问题

再看看iskindof_的源码:

local iskindof_
iskindof_ = function(cls, name)

    --cls={__index=B},
    --name="ClassA"

    local __index = rawget(cls, "__index")
    if type(__index) == "table" and rawget(__index, "__cname") == name then return true end

    if rawget(cls, "__cname") == name then return true end

    local __supers = rawget(cls, "__supers")  
    --{__index=B},这个表有__supers么?肯定没有。所以问题就是出在传入的cls有问题。

    if not __supers then return false end
    for _, super in ipairs(__supers) do
        if iskindof_(super, name) then return true end
    end
    return false
end

可以看到iskindof_用rawget找{__index=B}里的__supers域,那肯定没有。那传什么参数给iskindof_呢?
再看看binst的结构,一个__class和一个{__index=B}。看看iskindof_的两个参数:cls和name。这个函数本来就是去查class的,我们却把一个元表{__index=B}传了进去。那何不把class本身传进去呢?

    function iskindof(obj, classname)
        local t = type(obj)
        if t ~= "table" and t ~= "userdata" then return false end

        local mt
        if t == "userdata" then
            if tolua.iskindof(obj, classname) then return true end
            mt = tolua.getpeer(obj)
        else       
            -- mt = getmetatable(obj)  
            -- 修改后
            mt = rawget(obj,"class")
        end
        if mt then
            return iskindof_(mt, classname) 
        end
        return false
    end

经过测试,iskindof(binst,"ClassA")就返回了true。

那这样改会不会有问题呢?觉得可能会有些问题。问题就是这个iskindof函数跟class耦合性变高了。不过,函数的最后iskindof_本身就是跟class息息相关的。所以这样改,个人觉得没什么问题。

其实知道这个原因,还有很多其他改法:比如我们也可以把binst的结构改掉,把它的metatable {__index=B}改成B。这样也是没有问题的。因为B的__index就是B本身。

这是cocos2d-x-3.8.1版本,算是最新的,但我疑问为什么这么明显的BUG没有人纠正呢?至今还不解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值