每个美感尚存的C++ coder,第一次看到Symbian C++的程序,第一反应是:这鬼代码怎么缩进的?接下来,所有人会有疑问应该是:函数和类上的乱七八糟的前后缀是啥意思?
娃再丑也是爸妈生的,生成这模样虽然很无奈,但确实也是事出有因。在我看来,Symbian命名法的核心出发点,就是为了更好的内存资源管理。C++的人肉内存管理模式,在给人以控制到字节的快感的同时,也带了了麻烦到每行代码的烦恼。命名法,就是Symbian设计者憋出来用来辅助管理内存资源的方式之一。
类命名
Symbian的类,通常都带着一个字母的前缀,比如C、M、T、R、H等等。
所有从CBase派生而来的子类,都以C开头,形如Cxxxx。每个正确设计的,非抽象(不可实例化)的C类,都只能在堆上分配。为了保证这一点,每一个可实例化的C类,都应该按照Symbian的二阶段构造模式。但当然,这可以有意外。比如一些派生自CCoeControl的控件对象类,会需要从Resource文件中构造类的成员对象(而不仅仅通过二阶段中的ConstructL方式来构造),这使得它可能不适合按照二阶段构造的方式来封装。
做过.Net或者Java的人应该都明白,保持一个单一根的类型系统有什么好处,.Net在没有泛型的日子里,就是通过这个共根来实现一些基础的容器和方法。但这个好处,在C++,尤其是Symbian C++中体现的并不明显。因为C++有void *(在Symbian中华丽的转身为TAny *),有模板,可以来做一些类似的事情。更重要的,在Symbian C++中,为了节约空间,把虚表的RTTI项给精简掉了,使得Symbian C++的类丧失了dynamic_cast的能力,从而导致整个Symbian在运行期的动态识别能力,很是孱弱。
所以说,之所以要从CBase类进行派生堆上对象,很重要的一个原因,就是为了内存管理。CBase做了一件很重要的事情,就是将拷贝构造函数和赋值函数设置成了私有。这意味着,所有从CBase派生的子类,都默认被阉割了一刀,失去了拷贝构造的能力。这是为了提醒所有使用者,C对象的浅拷贝是不受欢迎的,如果你想提供该对象的拷贝功能(要深拷贝,不要浅...),往往是利用一些CloneL之类的接口来实现,保证行为的统一性。
C的类们,都涌向了堆中,栈上的活,留给了T类来完成。T类没有什么特殊的继承结构,每个T类,需要可以随意的在堆上或者栈上分配。大部分时候,它们该待的地方是栈,在栈上分配,并可以快速拷贝,一旦被析构,所以资源被释放,生不带来死不带走不留下一点残渣。因此,它们不应该包含大块的数据对象,但却可以拥有很复杂的接口,增加操作的便利性。比如,TRgb、TRect之类的系统类,就是典型的人小鬼大的代表人物。但T类不是C++的old plain类,它可以有继承结构,比如Symbian中描述符的那一堆堆T类,就拥有复杂的继承结构。
但世界是残酷的,有的类,偏偏就是投错了胎,搞得人不人,鬼不鬼。HBuf,就是此中代表。为了保持队形,维系接口,HBuf派生自TDesC,用以表示分配在堆上的Symbian描述符(就是字符串...)。但与一般T类不同,因为其占用空间动态变化,它必须在堆上分配,所以丧失了叫T的权利;另一方面,为了接口,它派生了T类,在排斥多根的C++中,它就不能够在从CBase派生了(继承的局限性,可见一斑),被断了叫C的后路。于是,就带上了H的特殊帽子,表示其在堆上分配,但不苟且于C类的屋檐下。
R类,换成通俗的描述,就是句柄类。它天生为了管理资源而存活,R类本身很简单,通常在栈上分配,可以拷贝,在这一点非常接近于T类。但与T类不同的时,R类往往带有某个堆对象的指针,指向文件之类的资源,或者是大块的堆数据对象。它析构的时候,默认是不析构这个指向的对象的,而是提供了一些类似于Close,Release之类的接口,需要人肉手动释放。有的T类也是指向另一块堆或者栈区域的,比如TPtr类。这两者一个本质的区别在于,T类指向的对象,不是它自己分配,它只是提供一个快捷方式,并不管指向对象的死活;而R类指向的资源,往往是自己本身或者另一个同类分配的资源,R类对象指向的资源,必须从这个R类的对象构造,从这个R类的对象析构(两个对象可以不同,但类是一致的)。
在Symbian C++中,还有一些类,不涉及任何内存资源。一个就是接口类,它们以M开头,相当于.Net的Interface,是一个纯虚类。每个Symbian中的类,可以派生自若干个M类,但仅仅能从一个有内存资源的对象那里进行分配。理论上,作为一个纯虚类,应该提供一个虚的析构函数,但在Symbian C++中,这往往是不需要的。因为在一个没有RTTI的世界里,只有第一个被继承的接口才有可能成功析构所有对象。比如一个类,形如 class A : public Cxxx, public Mxxx。只有用Cxxx接口才能管理资源,对Mxxx接口进行delete,完全没有办法释放全部资源(除非Cxxx里面没有任何数据...)。而Symbian的堆对象往往派生自CBase,所以,不可能从一个M类来析构对象,这个析构函数成不成虚,就是无关紧要的事情了。
另外一个不含任何资源的类,就是静态类了,在Symbian C++中它们没有任何前缀,是唯一不戴帽子的家伙。这个和.Net的static class一样,只包含一堆的静态方法,需要屏蔽所有构造、析构、拷贝接口(要没有这个闲工夫,不屏蔽也无所谓了...)。虽然,C++有函数,但出于对面向对象的热衷,使用这样的静态类,还是很值得鼓励的。。。
函数命名
在Symbian中,类是戴帽子的,函数则是穿裤子的。在Symbian的函数(包括成员和非成员函数)中,常有两种后缀,一个是L,另一个是LC。L,就是告诉你,这个函数可能Leave,换人类可知的语言描述,就是这个函数会抛出异常,需要谨慎处理。L是有传递性的,如果在调用该函数的地方对此L不理不睬放任其Leave,那么,在此调用函数后面,也需要添加一个L。
除了L,还有跟进一步的LC。这通常都是构造性的函数,它告诉你,它构造的过程中,不但可能Leave,并且分配的对象处于清理栈中。这是一个接近于语法糖的功能,如果在本函数中的后续部分需要调用被构造对象的相关接口,应该用LC,然后自己pop,而不是L。
其他命名法
还有一些对象,是会被带着前缀的。比如对象的成员变量,都带着前缀i;函数参数,都带着a(如果后面是原因字母开头,则需要用an,*_*)。在成员变量加前缀,这是常用的手段,可以和成员变量区分开了,帮助节约命名一个变量的脑细胞。但对函数形参加前缀,就是一件很诡异的事情了,剖有画蛇添足的艺术气息。
在Symbian中,所有的常量,都应该是K开头的,包括定义的const量,_LIT定义的字符常量等等。而枚举类型,同属于T类型,以T开头,其中的枚举值,则是以E开头。给这些类型的东西建立命名法,是常见的手段,只是Symbian不走寻常路,命名方式上不屑于与别人苟同。。。
结语
简而言之,Symbian制定了一套复杂的命名法规则,期待以此来规范化内存管理等操作。但世界的残酷在于,一个没有强制的标准,是不可靠的。命名法是一种弱约束的东西,工期赶的再急,也不可能无视编译和运行时的错误,但却可以无条件的忽视命名规则。并且,命名法是有强烈的破窗效应,一旦某一个函数没有合理的添加L,所有直接和间接调用它的函数,都可能会错误使用它,从而埋下隐患。况且,Symbian的命名法也算是枝繁叶茂了,很容易让人看不清楚端倪,不知不觉的就用错了,一个团队每个人在这上面犯一些错误,到最后命名法就完全丧失了效能。不过,就算是环境恶劣,对于个人而言,还是应该严于律己的,不论如何,不要轻易抛弃正确命名,这样,才可能造福大家。。。