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” |
__supers | nil |
super | nil |
__index | A |
metatable | { __index=一个总是返回nil的function } |
再看看B的结构:
B | |
---|---|
__cname | “ClassB” |
__supers | {A} |
super | A |
__index | B |
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 = index
和setmetatable(t,mt)
。意思是将instance的metatable设成{__index=index}
。
综上,得到binst的结构:
binst | |
---|---|
class | B |
metatable | { __index=B } |
原来binst也是有metatable:{__index=B}
。看上去还蛮简单的嘛。
说了那么多,最后返回来看我们的iskindof到底有什么问题?
再来看iskindof
iskindof的大致流程为:
我们传入的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没有人纠正呢?至今还不解。