关于RTTI
正如侯杰所说,文档视图是MFC进化为应用程序框架的灵魂,不可否认,这是MFC最为精炒的设计,十多年前数据与表现分离的思想就被应用在这个框架之上。而在文档视图之下,支撑着它的是运行时类型信息(RTTI)。
RTTI允许程序在运行时刻获得类乃至普通类型的信息,这是怎么做到的,其实原理很简单,就是事先将这些信息保存为某种数据结构,保存的工作或由编译器帮你做,或由程序自己完成。程序运行的时候通过某种途径取得这些信息,然后根据这些信息来做运行时判断。
Delphi是一种RTTI非常强的原生语言,通过TypeInfo函数取得类型信息的进入点,然后你就可以判断它是整型还是字符串,是类还是其他的什么东西。如果是类,你可以取得类名,Published段的成员和方法地址,甚至你可以动态设置属性的值。看看它的快速开发环境,以及组件机制,都离不开RTTI的支持。Java和C#就更强了,C#的反射机制允许查询一个类的所有字段,方法属性以及事件。RTTI反映了一种语言的动态性。
那么是否RTTI越强就越好呢,不同的应用领域结论会有所不同,RTTI越强表明你要额外存储的信息就越多,对于上层应用来说当然无关紧要,但对于一些底层的开发,空间效率都是必须仔细考虑的,RTTI反而成了一种负担。
C++几乎是一种全能的语言,它要面对的是各种各样的应用,因而在RTTI这个问题上就不得不慎重行事,因此C++的类型信息仅止于类型的判断,当然你可以扩展,但Bjarne建议尽量少用RTTI。事实上C++背负了太多历史包袱,对很多新特性的支持都是举步维艰的,比如垃圾回收,Bjarne说过:“垃圾回收将使C++不适合做许多底层的工作,而这却正是它的一个设计目标…如果原来就把垃圾回收作为C++的一个有机组件部分,那么C++早就可能成为死胎了。”
回到MFC,它并没有使用C++自带的类型信息,可能是那些类型信息根本不够用,也可能是MFC出来的太早(类型信息在1993年才成为C++的一部分)。MFC自己设计了一套RTTI机制,并用几个宏来简化代码的编写。
MFC的RTTI
MFC中保存类型信息的是一个叫CRuntimeClass的结构体,声明如下:
struct CRuntimeClass
{
//类名
LPCSTR m_lpszClassName;
//类实例大小
int m_nObjectSize;
//是否支持序列化的标志
UINT m_wSchema;
//创建类实例的函数指针
CObject* (PASCAL* m_pfnCreateObject)();
//指向基类的运行时结构
CRuntimeClass* m_pBaseClass;
//创建类实例
CObject* CreateObject();
//判断是否从某个类继承而来
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
//序列化支持
void Store(CArchive& ar) const;
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);
//组成运行时结构链表
CRuntimeClass* m_pNextClass;
};
一个具备类型信息的类都必须生成这个运行时结构,并将它保存为自己的静态成员。这个结构提供了三个方面的支持:
1. 类的基本信息,比如类名,对象尺寸,继承关系等。
你可以轻易判断两个类实例是否属于同一个类,比较m_lpszClassName,或比较类的RuntimeClass成员指针。
你可以通过IsDerivedFrom判断两个类的继承关系,前提是这两个类都生成了相应的RuntimeClass。
2. 动态创建支持,将类的一个静态成员函数保存进m_pBaseClass,而该函数必然要进行New的操作,比如:
CObject* CTest::CreateObject()
{
return new CText;
}
以后我们用pRuntimeClass->CreateObject()来动态创建对象。
3. 序列化支持,这个以后再说。
现在假设有一个CMyMainFrm类需要类型信息,它的声明可能是这样:
class CMyMainFrm : public CFrameWnd
{
public:
//声明一个针对这个类的运行时结构体
static const CRuntimeClass classCMyMainFrm;
//多态支持
virtual CRuntimeClass* GetRuntimeClass() const;
//动态创建支持
static CObject* PASCAL CreateObject();
public:
CMyMainFrm();
};
实现则是这样:
CObject* PASCAL CMyMainFrm::CreateObject()
{
return new CMyMainFrm;
}
const CRuntimeClass CMyMainFrm::classCMyMainFrm = {
"CMyMainFrm",
sizeof(class CMyMainFrm),
0xFFFF,
CMyMainFrm::CreateObject,
(CRuntimeClass*)(&CFrameWnd::classCFrameWnd),
NULL};
CRuntimeClass* CMyMainFrm::GetRuntimeClass() const
{
return (CRuntimeClass*)(&CMyMainFrm::classCMyMainFrm);
}
CMyMainFrm::CMyMainFrm()
{
}
编写这些代码之后,CMyMainFrm便具备了类型信息,以后通过GetRuntimeClass取得运行时结构就可以在运行时做一些事情了。
与消息映射一样, 每一个类都要写这一堆代码实在是麻烦,使用宏简化一下吧,于是有了DECLARE_DYNCREATE 和IMPLEMENT_DYNCREATE宏;有些类可能只需要基本的类型判断,而不需要动态创建,于是有了DECLARE_DYNAMIC和IMPLEMENT_DYNAMIC宏;有些类还需要序列化的能力,于是有了DECLARE_SERIAL和IMPLEMENT_SERIAL。
其中SERIAL包括DYNCREATE,DYNCREATE包括DYNAMIC,总结起来如下表所示:
宏 | 说明 |
DECLARE_DYNAMI IMPLEMENT_DYNAMIC | 基本类信息判断 |
DECLARE_DYNCREATE IMPLEMENT_DYNCREATE | 基本类信息判断 动态创建 |
DECLARE_SERIAL IMPLEMENT_SERIAL | 基本类信息判断 动态创建 序列化支持 |
以DYNCREATE为例,CMyMainFrm的声明和实现变成下面这样子:
class CMyMainFrm : public CFrameWnd
{
DECLARE_DYNCREATE(CMyMainFrm)
public:
CMyMainFrm();
};
IMPLEMENT_DYNCREATE(CMyMainFrm, CFrameWnd)
CMyMainFrm::CMyMainFrm()
{
}
类型信息使MFC有了一定的动态性,但这种动态性是受到限制的,要达到快速开发的级别仍然很难。不过有它的辅助已经可以完成很多高级的设计工作,比如文档视图,而这将是后面的主题。