cocos2d-x中lua-binding的面向对象开发的研究

起因

网路上已经有很多关于lua面向对象开发的文章,为什么我要自己写一篇呢?

一切都是因cocos2d-x 3.2的lua绑定而起。我们的新项目打算用lua进行开发,一直在跟进quick-x项目,从独立、第三方,到现在的被触控收编。cocos2d-x 3.2是最新的稳定版,把quick-x的很多好东西吸纳进来。与其说吸纳,不如说是直接把源代码拿过来了。所以,现在cocos2d-x 3.2直接就支持lua的面向对象开发,提供了原来只有quick-x才有的class等方法来方便的创建对象以及做继承。

既然cocos2d-x官方都认为quick-x的东西很好,我也不得不研究一下这套针对lua开发的创建类和继承的方法。不研究不要紧,一研究吓一跳。备受关注的quick-x,把自己宣传的那么好,那么便捷(我刚开始研究quick-x的时候,用它写过一个项目,提供的接口确实很便捷,大大提高了开发效率,但是没有研究实现细节),没想到最最底层,最最基础的部分,创建类、类继承的方案却如此业余,简直是一个刚学lua两三天的“高手”写出来的。说刚学两三天,是因为,从这个方案可以看出,完全没有理解lua语言的真谛,没有做到thinking in lua,而“高手”并不是贬义,一般人真的做不出这样的设计,写不出这样的代码。

分析

当然,也不是说这份代码一无是处(下个章节会详细分析),我这里先说说这份代码的优点和缺陷,仅代表个人意见,希望能引起共鸣。

优点

  1. 分解了创建实例(cls.__create)和构造函数的方法(instance:ctor),这样做的好处很明显,让混沌的思路清晰起来,并且“支持”从function继承,这也是支持从cpp对象继承的基础。
  2. 创造了类与类的实例之间的区别
    • 类:cls.class == nil
    • 类的实例:instance.class ~= nil
  3. 支持从cpp、lua的table继承,也支持创造新的类
  4. 支持默认构造函数
    • 当创建新的类时,有一个什么都不做的构造函数
    • 当lua的table继承时,可以调用父类的构造函数

缺陷

  1. 不支持多继承,这对于从cpp开发转过来的开发人员来说,可能是最大的问题,因为lua的特性其实是可以支持多继承的
  2. 没有充分利用lua的特性(下面的review详细展开讲)
    • 从lua的table继承时,不需要做clone,clone会失去很多好处,并且带来额外的内存开销
  3. 构造函数在继承过程中存在设计缺陷(下面的review会展开讲)

小结

所以总体上来看,还是优点多于缺点的。但是对于用惯了多继承(即使是不支持多继承的语言,比如Java、C#也是支持多接口实现的)的我来说,这点实在不可忍,所以不得不自己写了一套继承机制,为了避免这篇文章太长,将在下一篇文章中公开。

代码review

下面是重头戏了,让我们来看看class函数的代码。

为了让没有使用过这个函数的读者也能读懂,先说一下函数的用法吧

总体上看,这个函数可以分为3个部分(我已经在代码中用注释标明)。

  1. 局部变量声明以及参数类型检查
  2. 从cpp类继承
  3. 从lua的table继承

局部变量声明以及参数类型检查

从cpp类继承

问题1:从父类做深度拷贝

这里的super已经是拷贝过cpp类(userdata)的所有元素,已经是个lua的table了,我认为这里可以完全当做从lua的table继承来处理,而没必要再做一次深度拷贝。

问题2:默认的空构造函数

先理一下代码走到这里的前提

  • super是function
  • 或者super是从cpp类继承的table

你想到了什么?对,如果super是从cpp类继承的table,那么这里的默认构造函数就屏蔽了父类的构造函数。当然,在构造函数ctor里面有办法调到父类的构造函数的,可以这么做self.class.super.ctor(self)。但是回过头来看第一个前提,super是function的时候,super是没有被赋值到self.class.super的,所以现在要调用父类的构造函数的话,莫非还要判断一下?

问题3:不能被重复利用的代码

由于lua的随意性,很容易同一份代码被“写了多次”,又由于lua的函数是第一类对象,也就是说函数是可以被动态创建的。那么所有代码中,被我标记了问题3的地方,这些地方的函数,虽然看起来就一份代码,但是被创建出来以后却全是不同的实例。试想一下,每一个新创建的类型(不是类的实例),都包含了一个冗余的代码块,得有多大的代价?(实际开销可能并不大,但总觉得是个浪费。)

问题4:创建实例时的深度拷贝

这是在干嘛呢?拷贝那么多遍干嘛?都已经拷贝一遍到cls里面了,再拷贝一遍到instance干嘛呢?完全没有thinking in lua嘛!这里只需要setmetatable就可以了呀。

从lua的table继承

问题5:从父类做深度拷贝

刚刚从userdata做深度还是可以理解的,但是现在是从lua的table做继承呀,放着setmetatable不用,又clone干嘛呀?

总结

很多观点可能会有人认为偏颇,因为大多是深度拷贝的问题,而深度拷贝的时候,对于table、function、userdata等数据来说,也只是拷贝一个引用,并没有太大的代价。但我始终固执地认为,既然要用lua,就应该用lua的思维来解决问题,特别是底层的部分,而且底层的部分,对于内存和执行效率的考虑应该更深入,更全面一些。

不过至少大家可以达成共识,最大的问题就是问题2。

最后,放出我自己写的Oop库来解决上面的问题,并且有更多惊喜等着你去发现。

8 comments

  1. 文章看了,写得不错。
    不支持多继承是故意的,因为quick希望大家用组件的方式来扩展功能,而不是多继承。
    深度拷贝有一处确实是多余的,这是源自早期tolua++的一个bug。
    这个bug在导出一个cpp class时,没有正确设置metatable的__index方法。因此当试图修改一个cpp class的方法时,会出现stack overflow。所以class方法里为了绕过这个bug,多做了一次拷贝,就是将cpp class的所有值复制到了一个lua table。
    后来在七月阳光同学加入quicj团队后,修复了这个tolua的bug。但考虑到当时cocos2dx里的这个bug还存在,所以继续保留了class的这处拷贝。
    文章里有一处提到需要判断父类是否存在,应该是早起版本的bug。估计作者看的是cocos2dx里的class函数。这是cocos团队提取了这个函数的老版本过去。

    1. @dualface 感谢廖大百忙之中抽空来回复。
      关于多继承,我个人认为还是有必要的,可能受golang的影响,很喜欢它的那种组合即“继承”的方式,虽然是语法糖,但可以更好的隐藏实现细节。不过这个问题可能很难达成一致,毕竟个人爱好的成分多一点。
      深度拷贝的问题,我想说的是,从userdata继承的深度拷贝是有价值的,但是从table继承,是不需要深度拷贝的,从userdata继承只有一处,其他全是从table继承。
      需要判断父类是否存在的问题,我对比了cocos2d-x的extern.lua以及quick-cocos2d-x的extern.lua,发现两个库中的clone和class函数是一模一样的,不知道廖大指的修复版本在哪里可以找到?

  2. 今天正好在更新框架,特别看了一下这里的代码。
    作者看的 class() 函数确实是老版本,那个找不到父类 ctor() 的 bug 是很早以前的了。quick 的 class() 定义在 framework/functions.lua 里。
    深度拷贝确实有多余的,就是为了绕过当时的 tolua++ bug,否则会导致 Stack Overflow。

    1. @dualface
      不知道cocos2dx3.2是不是还在用有问题的tolua++?我试图改掉这个问题,但没有成功…希望有机会能跟廖大直接讨教这个问题

  3. 早就已经能不用集成就不用继承了。纯组合+原生Lua的UserData方式使用。Lua不是CPP,这么折腾都是在误用Lua。

    1. @fanfeilong
      前面回复 @dualface 就提过,是受到golang影响的关系,golang是组合即“继承”。我试图把lua打造成“继承”即“组合”。
      所谓继承在lua里只是概念,并没有直接的语法支持,但我认为,如果要使用lua作为主要编程语言,使用这个概念不可避免。

Leave a Reply

Your email address will not be published. Required fields are marked *