二、Lua中类的简单实现
Lua的设计初衷并非意图构建完整的应用,而是嵌入在应用程序中为应用提供灵活的扩展和定制功能,所以Lua仅提供了基本的数学运算和字符串处理等函数库,而并未涵盖程序设计的方方面面。会让你惊讶的是,在面向对象概念已经泛滥的今天,lua作为新兴脚本语言其甚至没有原生态的提供对面向对象的支持,说简单点是lua没有class相关的关键字,其也不支持定义一个类,更别提多态了。
不过读者肯定注意到了上面那句话中的“原生态”三个字,是的,原生态的lua中是没有类这个概念的。不过lua提供的table(表)这个强大的数据结构却赋予了程序员自行实现一个面向对象意义上的class的能力。
废话休提,言归正传。先来看看coco2d-x 3.0所给出的官方的class的实现。
这里不对此实现方式做过多讲解,重点将一些继承的实现。此class的实现中,子类继承父类时会将父类中所有的对象(变量或者函数)拷贝到子类中,也即这里做的:
从此子类和父类再无联系(除了子类的.super变量指向父类外),子类若调用继承自父类的函数实际调用的是从父类中一比一复制过来的函数。若子类中再自己覆盖了父类的同名函数,则之后在子类中再没有办法调用到子类继承自父类的此函数:
你或许会说,难道我不可以这样吗?
按照你的期待,上述代码的输出应该是:
cnt is 1
cnt is 2
cnt is 1
cnt is 2
或者你可能考虑到这里继承时将所有父类的对象拷贝到了子类中,因此子类对象中应该有一个自己的cnt和一个继承自父类的cnt,因此上述代码中self.super:show()中用到的self._cnt或许没有动态绑定到子类中的cnt,因此输出内容更应该是这样的:
cnt is 1
cnt is 1
cnt is 1
cnt is 1
可惜让你大跌眼镜的是,实际的输出内容却是这样的:
关于上述输出的原因,我这里留给读者自己去给出。这里简单的阐述一个概念,即lua中的table概念及其重要。上述类的实现导致类和类实例化出的对象其实都是table,而且它们的地位是平等的。也即对象和类在数据结构层次是一模一样的东西(也就是一个table),类是一个table,对象是一个table,类实例化生成对象时只是把自己内部的对象拷贝了一份到对象中。因此上述代码中childObejct1:show()和childObject2:show()函数体内的self.super:show()其实访问的是同一个函数,且该函数内的self._cnt也是同一个对象。
这种类实现简洁清晰却功能极其有限,基本体会不到类继承的意义,因此除非你的项目目标代码量不会超过1万行,否则就不要使用上述类的实现。
再来看看云风(吴云洋)大大在他的博客上给出的实现(什么你问我云风是谁?你你你真的是想做游戏的吗?),这里先直接贴出代码。
博客中顺带给出了简单的使用范例:
云风作为网易游戏技术总监,又同时是游戏技术界的传奇人物,他写出来的代码绝对让人叹服。这段代码有几处的设计极为巧妙,用了最少的代码实现了强大的功能同时代码结构还非常清晰干练,下面我们挑几处来分析:
首先看类的new函数,这里每定义一个class都会给它生成一个new函数,之后就可以通过className.new(…)来创建对象。
New函数里定义了一个递归函数create,该create判断传入的c(也即当前类)是否存在super(也即父类)如果存在则递归调用,这样一来就沿着类的递归链将类所有的父类自上而下传入create函数。而之后则调用其ctor(也即构造函数,如果存在)对对象进行构造,重点在这里,这里调用构造函数传入的第一个参数self是obj,也就是new函数第一句话申明出的局部对象。如此调用则在类(包含当前类及其所有父类,下同)的ctor中声明的变量通通都在obj中被创建,就成功的实现了将对象初始化(当然这也意味着不在类ctor中声明的变量不会在obj中被创建)。且在类ctor申明的变量的创建延迟到了对象被创建时因而这部分变量也不会在类中,相对那些现在类中创建变量之后实例化时把类中所有变量拷贝一份到实例化对象中的方法而言避免了一定的无意义内存开销。
函数的结尾是一句:
setmetatable(obj,{__index = _class[class_type] })
可能有部分读者会嘀咕,_class[class_type]是什么,我这里还看到相关代码啊?别急,下文马上就有了。
这里看到_class[class_type]其实指向了一个表vtbl(名字的命名来源于c++类对象中的虚表),该vtbl被设置为class_type的元表且对class_type的__newindex操作被hook到了在vtbl上进行。
这里逻辑初步看起来不知其所以然,其真实目的是这样一做,之后在类(也就是这里即将被返回的class_type表)中添加的任何属性(变量)或方法(函数)都被实际在vtbl中创建,这时候回过头看看.new方法中的那句就瞬间明白了——这样一来就可以通过对象来访问到类中的方法了(当然也包括那些不在类的ctor中被申明的变量)。
因此示例中test类在被class(base_type)创建出后添加的hello()方法,就能通过对象a:hello()来访问到。
再来看看代码的最后一部分:
这里是用来实现类的继承逻辑的,test类继承自base_type类,test中的vtbl只保证了通过对象a能够访问到test中添加的方法,但是对于那些在test的父类base_type中的方法(比如例子中的print_x())就得靠这里来访问。这里给vtbl再设置了一个元表,其中__index原方法指向的就是父类的vtbl(这里保存有父类中的方法),因此最终的对象访问一个方法(比如print_x()),在其直接类(比如test)的vtbl中找不到时会向上到类的父类的vtbl中找,并如此递进直到找到了或者确定不存在为止。
Vtbl[k]= ret这句是在第一次在父类中查找时把查找结果拷贝到当前类,从而避免了下一次访问的重复查找。
另外需要注意的是,对于那些不在类的ctor()函数中申明的变量因为会保存在类的vtbl表中,该表对类唯一因而为类所有对象所共有。因此这种变量的性质有点类似c++中的静态变量。不过这里只能称之为”伪静态变量“,原因在于多层继承时,父类的这种变量在被最终对象第一次访问拷贝了一份,从而失去了全局唯一的性质。因此这里我们只能将它们称作“伪静态变量”。