第一章 DELPHI的原子世界
"天苍苍,野茫茫,风吹草低 见牛羊"
在使用DELPHI开发应用软件的过程中,我们就像草原上一群快乐牛羊,无忧无虑地享受着Object Pascal语言为我们带来的温暖阳光和各种VCL控件提供的丰富水草。抬头望望无边无际蔚蓝的天空,低头品尝大地上茂密的青草,谁会去想天有多高?地有多大?阳光和水草又是从何而来?那是大师关心的事。而大师此时正坐在高高的山顶上,仰望宇宙星云变换,凝视地上小虫的爬行。蓦然回头,对我们这群吃草的牛羊点头微笑。随手扯起一根小草,轻轻地含在嘴里,闭上眼睛细细品尝。不知道这根青草在大师的嘴里是什么味道?只是,他的脸上一直带着满意的微笑。
第一节 System
不经意,偶然打开了System.pas的原程序文件,却发现这里竟是一个既熟悉又陌生的世界。在这里有我们熟知的东东,如:TObject、TClass、GUID、IUnknown、IDispatch ……但这些东西也是我们所陌生的。在茫茫编程生涯中,我们不断地与这些东东打交道,都已经熟悉得宛如自己身体的一部分。但真想要去了解他们,也就人象想要了解自身一样的茫然。
在System.pas单元的开头,有这样一段醒目的注释文本:
{ Predefined constants, types, procedures, }
{ and functions (such as True, Integer, or }
{ Writeln) do not have actual declarations.}
{ Instead they are built into the compiler }
{ and are treated as if they were declared }
{ at the beginning of the System unit. }
这段话的意思是说:"这一单元包含预定义的常量、类型、过程和函数(诸如:Ture、Integer或Writeln),它们并没有实际的声明,而是编译器内置的,并在编译的开始就被认为是已经声明的定义"。
System单元不同于别的单元。你可以将Classes.pas或Windows.pas等其他DELPHI源程序文件加入你的项目文件中进行编译,并在源代码基础上调试这些单元。但你绝对无法将System.pas源程序文件加入到你的项目文件中编译!DELPHI将报告"重复定义了System单元"的编译错误。
任何DELPHI的目标程序中,都自动包含System单元中的代码,哪怕你的程序一句代码也没写。看看下面的程序:
program Nothing;
begin
end.
这个程序用DELPHI 6编译之后有8K,用DELPHI 5编译之后有16K。而使用过C语言的朋友都知道,最简单的C语言程序编译之后是非常短小的,有的不到1K。但DELPHI不是的。
这个什么也不做的程序怎么会有8K或16K的长度呢?这是因为其含有System单元的代码。虽然这些代码没有C或C++语言的启动代码那样短小精悍,但里面却包含支撑整座DELPHI大厦的基石,是很牢靠的。
在DELPHI6中,Borland为了兼容其在Linux下的旗舰产品Kylix,进一步精简了System单元的基础程序,将一部分与Windows系统相关的内容移到了别的单元。所以,上面最简单的程序经过DELPHI6编译生成的目标程序就比DELPHI5生成的小的多。其实,DELPHI 6中的System.pas单元有一万八千多行源程序,比DELPHI 5的多得多。这是因为在DELPHI6的那些支持Kylix的单元中,有些代码同时写了两个版本,一个支持Windows,一个支持Linux,并在编译宏命令的控制下生成各自操作系统的目标程序。Borland完成这些程序改写之后,就有可能将DELPHI编写的程序移植到Kylix上。按照Borland提供的某些原则编写的DELPHI程序可以不用修改直接在Kylix上编译,并在LINUX系统上运行。这对需要进行跨平台开发的程序员来说无疑是个福音。目前,在真编译的可视开发工具中,DELPHI 6和Kylix恐怕是唯一能实现跨平台编译功能的开发工具。
走马观花,浏览一下DELPHI的源代码是值得的。因为,DELPHI的源代码中蕴藏着丰富的营养,那都是大师们的杰作。如果,我们开发的应用应用程序是一棵开花的树,那么,请在我们拥有这份花满枝丫的浪漫时,请不要忘了深埋在土壤里的那一藤树根。没有树根提供营养,就没有烂漫的花枝。要知道,世界上任何一棵树的树根总比其树冠更多,更茂盛,尽管人们看不到深埋在地下的树根。
但浏览DELPHI的源程序也是很费精力的。虽然,大师们写的程序大都风格一流,易于阅读和理解,但代码实在太多。阅读System.pas单元就更不容易,其中的大量程序甚至是用汇编语言编写的,这对有些朋友来说无异于天书。我们无意逐一去解读其中的奥秘,这可能会耗用我们九九八十一个不眠之夜。但我们总能学到一些编程风格,了解其中的一些内容,并能悟得一些道理,而这可能会让我们受益终身。
当然,我无意将DELPHI的源代码神化为圣典。因为,那也毕竟不是天书,也是人编写的,也能抓到其中的几只臭虫。但我们自己又怎样呢?
第二节 TObject
TObject是什么?
TObject在DELPHI中就是与生俱来的东西,没有什么好问的。
不知道TObject是什么,照样可以编写出很好的DELPHI程序。我们可以小心苛护自己的DELPHI程序,"朝朝勤拂拭,莫让惹尘埃",我们的程序也能照样欢快地奔跑。世界上有很多的东西都是我们不知道的,我们一样也生活得很好。
但世上总有些人就是喜欢去学习和探索那些不知道的东西,最终他们知道的东西总比别人多些,成为了智者。我想,在编程中也是这样,如果经过我们不断地学习和探索,将不知道的东西变成我们知道的东西,我们也会逐渐成为编程中的智者。相信总有一天能进入"本来无一物,何处惹尘埃"的境界。
TObject是System单元中定义的第一个类。由此可见它在DELPHI中的重要性。TObject的定义是这样的:
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult; virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
TObject还真有不少东东。
注意,TObject是class类型。
说到这里,也许有人要问这需要特别注意吗?
在此,我只是想提醒大家不要忘了,在Object Pascal语言中还有一种以object保留字定义的对象类型。这种数据板块套上过程作为方法的老古董,同样实现了面向对象的各种特征,只不过它并非现代DELPHI大厦的奠基石。有点象是历史文化遗产,属于传统文化系列。但了解历史可以更深刻地理解现在并展望未来。现在,class 系列的对象类才是DELPHI的基础,它和对象的接口技术一起,支撑起整个DELPHI大厦。我们所讲的对象几乎都是class系列的。所以如果没有特别指明,"对象"一词都指class类型的对象。
我们都知道,在DELPHI中TObject是所有class系列对象的基本类。也就是说,在DELPHI中,TObject是万物之源。不管你自定义的类是否指明了所继承的父类,一定都是TObject的子孙,一样具有TObject定义的所有特性。
那么,一个对象到底是什么?
对象就是一个带柄的南瓜。南瓜柄就是对象的指针,南瓜就是对象的数据体。确切地说,DELPHI中的对象是一个指针,这个指针指向该对象在内存中所占据的一块空间。
虽然,对象是一个指针,可是我们引用对象的成员时却不能写成这样的代码:
MyObject^.GetName;
而只能写成:
MyObject.GetName;
这是Object Pascal语言扩充的语法,是由编译器支持的。使用C++ Builder的朋友就很清楚对象与指针的关系,因为在C++ Builder的VCL对象都是通过指针引用的。
为什么说对象是一个指针呢?我们可以试着用sizeof函数获取对象的大小,例如计算sizeof(MyObject)的值。结果是4字节,这就就是一个32位指针的大小,只是南瓜柄的大小。而对象的真正大小应该用MyObject.InstanceSize获得,这才是南瓜应有的份量。广义的说,我们常用的"句柄"概念,英文叫Handle,也是一个对象指针,因为它后面也连着一个别的什么瓜。
既然DELPHI对象是指向一块内存空间的指针,那么,代表对象的这快内存空间又有怎样的数据结构呢?就把南瓜切开来看看啰。
我们将对象指针指向的内存空间称为对象空间。对象空间的头4个字节是指向该对象直属类的虚方法地址表(VMT - Vritual Method Table)。接下来的空间就是存储对象本身成员数据的空间,并按从该对象最原始祖先类的数据成员到该对象具体类的数据成员的总顺序,和每一级类中定义数据成员的排列顺序存储。
每一个类都有对应的一张VMT,类的VMT保存从该类的原始祖先类派生到该类的所有类的虚方法的过程地址。类的虚方法,就是用保留字vritual声明的方法。虚方法是实现对象多态性的基本机制。虽然,用保留字dynamic声明的动态方法也可实现对象的多态性。但这样的方法不保存在VMT中。用保留字dynamic声明的动态方法只是Object Pascal语言提供的另一种可节约类存储空间的多态实现机制,但却是以牺牲调用速度为代价的。
即使,我们自己并未定义任何类的虚方法,但该类的对象仍然存在指向虚方法地址表的指针,只是地址项的长度为零。可是,在TObject中定义的那些虚方法,如Destroy、FreeInstance等等,又存储在什么地方呢?原来,他们的方法地址存储在相对VMT指针负方向偏移的空间中。在VMT的负方向偏移有76个字节的数据信息,它们是对象类的基本数据结构。而VMT是存储我们自己为类定义的虚方法地址的地方,它只是类数据结的构扩展部分。VMT前的76个字节的数据结构是DELPHI内定的,与编译器相关的,并且在将来的DELPHI版本中有可能被改变。
下面的对象和类的结构草图展示了对象和类之间的一些关系。
TObject中定义的有关类信息或对象运行时刻信息的函数和过程,一般都与类的数据结构相关。
在DELPHI中我们用TObject、TComponent等等标识符表示类,它们在DELPHI的内部实现为各自的VMT数据。而用class of保留字定义的类的类型,实际就是指向相关VMT数据的指针。
对我们的应用程序来说,类的数据是静态的数据。当编译器编译完成我们的应用程序之后,这些数据信息已经确定并已初始化。我们编写的程序语句可访问类数据中的相关信息,获得诸如对象的尺寸、类名或运行时刻的属性资料等等信息,或者调用虚方法以及读取方法的名称与地址等等操作。
当一个对象产生时,系统会为该对象分配一块内存空间,并将该对象与相关的类联系起来。于是,在为对象分配的数据空间中的头4个字节,就成为指向类VMT数据的指针。
我们再来看看对象是怎样诞生和灭亡的。我们都知道,用下面的语句可以构造一个最简单对象:
AnObject := TObject.Create;
编译器将其编译实现为,用TObject对应的类数据信息为依据,调用TObject的Create构造函数。而TObject的Create构造函数调用了系统的ClassCreate过程。系统的ClassCreate过程又通过调用TObject类的虚方法NewInstance。调用TObject的NewInstance方法的目的是要建立对象的实例空间。TObjec类的NewInstance方法将根据编译器在类信息数据中初始化的对象实例尺寸(InstanceSize),调用GetMem过程为该对象分配内存。然后调用TObject类InitInstance方法将分配的空间初始化。InitInstance方法首先将对象空间的头4个字节初始化为指向对象类的VMT的指针,然后将其余的空间清零。建立对象实例最后,还调用了一个虚方法AfterConstruction。最后,将对象实例数据的地址指针保存到AnObject变量中,这样,AnObject对象就诞生了。
同样,用下面的语句可以消灭一个对象:
AnObject.Destroy;
TObject的析构函数Destroy被声明为虚方法,这可以让某些有个性的对象选择自己的死亡方法。Destory方法首先调用了BeforeDestruction虚方法,然后调用系统的ClassDestroy过程。ClassDestory过程又通过调用对象的FreeInstance虚方法。由FreeInstance方法调用FreeMem过程释放对象的内存空间。就这样,一个对象就在系统中消失。
对象的析构过程比对象的构造过程简单,就好像生命的诞生是一个漫长的孕育过程,而死亡却相对的短暂,这似乎是一种必然的规律。
在对象的构造和析构过程中,调用了NewInstance和FreeInstance两个虚函数,来创建和释放对象实例的内存空间。之所以将这两个函数声明为虚函数,是为了能让用户在编写需要用户自己管理内存的特殊对象类时(如在一些特殊的工业控制程序中),有扩展的空间。
而将AfterConstruction和BeforeDestruction声明为虚函数,也是为了将来派生的类在产生对象之后,有机会让新诞生的对象呼吸第一口新鲜空气,而在对象消亡之前可以允许对象交待最后的遗言,这都是合情合理的事。例如,我们熟悉的TForm对象和TdataModule对象的OnCreate事件和OnDestroy事件,就是分别在这两个重载的虚函数中触发的。
此外,TObjec还提供了一个Free方法。它不是虚方法,它是为了在搞不清对象指针是否为空(nil)的情况下,也能安全释放对象而专门提供的。当然,搞不清对象指针是否是否为空,本身就有程序逻辑不清晰的问题。不过,任何人都不是完美的,都可能犯错,使用Free能避免偶然的错误也是件好事。然而,编写正确的程序不能一味依靠这样的解决方法,还是应该以保证程序的逻辑正确性为编程的第一目标。
有兴趣的朋友可以读一读System单元的原代码,其中,大量的代码是用汇编语言书写的。细心的朋友可以发现,TObject的构造函数Create和析构函数Destory竟然没有写任何代码。其实,在调试状态下通过Debug的CPU窗口,可清楚地反映出Create和Destory的汇编代码。我想,可能是因为缔造DELPHI的大师门不想将过多复杂的东西提供给用户。他们希望用户在简单的概念上编写应用程序,将复杂的工作隐藏在系统的内部由他们来承担。所以,在编写System.pas单元时特别将这两个函数的代码去掉,让用户认为TObject是万物之源,用户派生的类完全从虚无中开始,这本身并没有错。
第三节 TClass
在System.pas单元中,TClass是这样定义的:
TClass = class of TObject;
它的意思是说,TClass是TObject的类。因为TObject本身就是一个类,所以TClass就是所谓的类的类。
从概念上说,TClass是类的类型,即,类之类。但是,我们知道DELPHI的一个类,代表着一项VMT数据。因此,类之类可以认为是为VMT数据项定义的类型,其实,它就是一个指向VMT数据的指针类型!
在以前传统的C++语言中,是不能定义类的类型的。对象一旦编译就固定下来,类的结构信息已经转化为绝对的机器代码,在内存中将不存在完整的类信息。一些较高级的面向对象语言才可支持对类信息的动态访问和调用,但往往需要一套复杂的内部解释机制和较多的系统资源。而DELPHI的Object Pascal语言吸收了一些高级面向对象语言的优秀特征,又保留可将程序直接编译成机器代码的传统优点,比较完美地解决了高级功能与程序效率的问题。
正是由于DELPHI在应用程序中保留了完整的类信息,才能提供诸如as和is等在运行时刻转换和判别的高级面向对象功能,而类的VMT数据在其中起了关键性的核心作用。有兴趣的朋友可以读一读System单元的AsClass和IsClass两个汇编过程,他们是as和is操作符的实现代码,这样可以加深对类和VMT数据的理解。
有了类的类型,就可以将类作为变量来使用。可以将类的变量理解为一种特殊的对象,你可以象访问对象那样访问类变量的方法。例如:我们来看看下面的程序片段:
type
TSampleClass = class of TSampleObject;
TSampleObject = class( TObject )
public
constructor Create;
destructor Destroy; override;
class function GetSampleObjectCount:Integer;
procedure GetObjectIndex:Integer;
end;
var
aSampleClass : TSampleClass;
aClass : TClass;
在这段代码中,我们定义了一个类TSampleObject及其相关的类类型TSampleClass,还包括两个类变量aSampleClass和aClass。此外,我们还为TSampleObject类定义了构造函数、析构函数、一个类方法GetSampleObjectCount和一个对象方法GetObjectIndex。
首先,我们来理解一下类变量aSampleClass和aClass的含义。
显然,你可以将TSampleObject和TObject当作常量值,并可将它们赋值给aClass变量,就好象将123常量值赋值给整数变量i一样。所以,类类型、类和类变量的关系就是类型、常量和变量的关系,只不过是在类的这个层次上而不是对象层次上的关系。当然,直接将TObject赋值给aSampleClass是不合法的,因为aSampleClass是TObject派生类TSampleObject的类变量,而TObject并不包含与TSampleClass类型兼容的所有定义。相反,将TSampleObject赋值给aClass变量却是合法的,因为TSampleObject是TObject的派生类,是和TClass类型兼容的。这与对象变量的赋值和类型匹配关系完全相似。
然后,我们再来看看什么是类方法。
所谓类方法,就是指在类的层次上调用的方法,如上面所定义的GetSampleObjectCount方法,它是用保留字class声明的方法。类方法是不同于在对象层次上调用的对象方法的,对象方法已经为我们所熟悉,而类方法总是在访问和控制所有类对象的共同特性和集中管理对象这一个层次上使用的。
在TObject的定义中,我们可以发现大量的类方法,如ClassName、ClassInfo和NewInstance等等。其中,NewInstance还被定义为virtual的,即虚的类方法。这意味作你可以在派生的子类中重新编写NewInstance的实现方法,以便用特殊的方式构造该类的对象实例。
在类方法中你也可使用self这一标识符,不过其所代表的含义与对象方法中的self是不同的。类方法中的self表示的是自身的类,即指向VMT的指针,而对象方法中的self表示的是对象本身,即指向对象数据空间的指针。虽然,类方法只能在类层次上使用,但你仍可通过一个对象去调用类方法。例如,可以通过语句aObject.ClassName调用对象TObject的类方法ClassName,因为对象指针所指向的对象数据空间中的头4个字节又是指向类VMT的指针。相反,你不可能在类层次上调用对象方法,象TObject.Free的语句一定是非法的。
值得注意的是,构造函数是类方法,而析构函数是对象方法!
什么?构造函数是类方法,析构函数是对象方法!有没有搞错?
你看看,当你创建对象时分明使用的是类似于下面的语句:
aObject := TObject.Create;
分明是调用类TObject的Create方法。而删除对象时却用的下面的语句:
aObject.Destroy;
难道不是吗?TObject是类,而aObject是对象。
原因很简单,在构造对象之前,对象还不存在,只存在类,创建对象只能用类方法。相反,删除对象一定是删除已经存在的对象,是对象被释放,而不是类被释放。
最后,顺便讨论一下虚构造函数的问题。
在传统的C++语言中,可以实现虚析构函数,但实现虚构造函数却是一个难题。因为,在传统的C++语言中,没有类的类型。全局对象的实例是在编译时就存在于全局数据空间中,函数的局部对象也是编译时就在堆栈空间中映射的实例。即使是动态创建的对象,也是用new操作符按固定的类结构在堆空间中分配的实例,而构造函数只是一个对已产生的对象实例进行初始化的对象方法而已。传统C++语言没有真正的类方法,即使可以定义所谓静态的基于类的方法,其最终也被实现为一种特殊的全局函数。更不用说虚拟的类方法,虚方法只能针对具体的对象实例有效。因此,传统的C++语言认为,在具体的对象实例产生之前,却要根据即将产生的对象构造对象本身,这是不可能的。的确不可能,因为这会在逻辑上产生自相矛盾的悖论!
然而,正是由于在DELPHI中有动态的类的类型信息,有真正虚拟的类方法,以及构造函数是基于类实现的等等这些关键概念,才可实现虚拟的构造函数。对象是由类产生的,对象就好象成长中的婴儿,而类就是它的母亲,婴儿自己的确不知道自己将来会成为什么样的人,可是母亲们却用各自的教育方法培养出不同的人,道理是相通的。
都知道强大的VCL是DELPHI得以成功的基础之一,而所有VCL的鼻祖是TComponent类。在TComponent类的定义中,构造函数Create被定义为虚拟的。这能使不同类型的控件实现各自的构造方法,这就是TClass创造的类之类概念的伟大,也是DELPHI的伟大。
第四节 运用TObject的方法
我们先来看看TObject的各个方法都是些什么东东。简要列示如下:
constructor Create;
TObject类的构造函数,用于建立对象。
procedure Free;
安全释放对象数据空间。
class function InitInstance(Instance: Pointer): TObject;
初始化新建对象的数据空间。
procedure CleanupInstance;
在对象被释放前清除对象的数据空间。
function ClassType: TClass;
获得对象直属的类。
class function ClassName: ShortString;
获得对象直属类的名称。
class function ClassNameIs(const Name: string): Boolean;
判断对象直属类的名称是否是指定的名称。
class function ClassParent: TClass;
获得对象或类的上一代类,即父类。
class function ClassInfo: Pointer;
获得对象类的运行时类型信息(RTTI),一般用于Tpersistent类。
class function InstanceSize: Longint;
获得对象实例的大小。
class function InheritsFrom(AClass: TClass): Boolean;
判断对象或类是否是从指定的类派生的。
class function MethodAddress(const Name: ShortString): Pointer;
获得对象或类指定方法名称的调用地址。该方法必须是published的。
class function MethodName(Address: Pointer): ShortString;
获得对象或类指定方法地址的方法名称。该方法必须是published的。
function FieldAddress(const Name: ShortString): Pointer;
获得对象指定属性名称的访问地址指针。
function GetInterface(const IID: TGUID; out Obj): Boolean;
获得对象支持指定接口标识的接口。
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
获得对象或类指定接口标识的接口项。
class function GetInterfaceTable: PInterfaceTable;
获得对象或类支持的所有接口项的信息表。
function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual;
支持接口对象safecall调用异常处理虚方法,常被接口对象重载。
procedure AfterConstruction; virtual;
对象建立后首先被调用的虚方法,供派生类对象重载以初始化新对象。
procedure BeforeDestruction; virtual;
对象释放前最后被调用的虚方法,供派生类对象重载以清理对象数据。
procedure Dispatch(var Message); virtual;
对象的消息处理方法,支持Windows等消息处理。
procedure DefaultHandler(var Message); virtual;
缺省的消息处理方法。
class function NewInstance: TObject; virtual;
分配对象实例空间的虚方法。
procedure FreeInstance; virtual;
释放对象实例空间的虚方法。
destructor Destroy; virtual;
对象的析构虚方法,用于消灭对象。
这些方法在DELPHI的帮助文档中都有描述。有些方法已在前面简单介绍过。由于TObject的方法也比较多,一时也讲不完。就挑一两个说说其用法吧,其他的在后面用到时再细细道来也不迟。
就说说MethodAddress和FieldAddress对象方法吧。
在使用DELPHI开发程序的过程中,我们经常会与VCL的属性和事件打交道。我们添加元件到设计窗口中,设置元件的相关属性,为元件的各种事件编制处理事件的方法。然后,轻轻松松地编译,程序就诞生了,一切都是可视化的。
我们知道,在设计时DELPHI将元件的数据成员(包括字段、属性和事件)等信息存储在*.DFM文件中,并将其作为资源数据编译到最终的执行程序中。DELPHI的编译过程同时也将源程序中类的结构信息和代码也编译到执行程序中,这些信息在运行时可以由程序访问的。
DELPHI的程序在运行时创建的Form或DataModule等对象时,首先建立该对象。接着,从相应的资源数据中读取设计时保留的数据成员信息,并使用FieldAddress方法获取数据成员的访问地址。然后,用设计时定义的值初始化该数据成员。如果是事件,则再调用MethodAddress获取事件处理程序的调用地址,并初始化该事件。这样就完成了设计时的数据代码关系到运行时的数据代码关系的映射,有点儿象动态连接过程。
原来,元件的某些的数据成员和方法是可以用名称去访问的,就是使用FieldAddress和MethodAddress方法。其实,这种功能是在最基础的TObject中就支持的。当然,只有定义为published访问级别的数据成员和方法才可以使用名称去访问,而定义为private、protected和public访问级别的除外。
注意,只有类型是类或接口的数据成员才可定义为published的访问级别,方法都是可以定义为published的。对于从TPersistent继承的那些对象类,如果没有特别声明数据成员和方法的访问级别的,则缺省是published的。例如,TForm类是Tpersistent派生下来的,一个典型的Form类的定义中,由DELPHI的IDE自动维护和生成的那些数据成员和方法,缺省都是published的。因为,TPersistent类使用了特殊的{$M+}编译选项。
知道这层内幕之后,我们也可以自己使用这些方法来实现一些有意义的功能。
第五节 对象的消息处理机制
TObject的定义中,有两个方法值得我们注意,就是:
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
这两个方法是DELPHI的VCL强大的消息处理机制的基础,Windows的各种消息最终都是通过这两个个方法处理掉的。
在讲述这一问题之前,有必要先说明一下什么是消息。
从广义上将,消息就是信息的传递,一个对象将自己知道的事情通知其他对象。每个对象可以根据得到的消息做出相应的反应。消息在现实世界中普遍存在,故事、新闻、命令、报告等等,当然也包括流言蜚语。在程序中表现为数据访问、过程调用、方法调用、事件触发和通讯协议等等。
而我们今天讨论的消息是狭义的消息,这种消息就是对象间的一种通讯协议。这种消息沟通机制的特点是,相关对象之间不会象变量访问和方法调用那样是固定的耦合关系,而是非常自由和松散的关系。采用这种消息机制,对象之间是的通讯方式是统一的,而消息的内容是多种多样的,一组通讯的对象之间可以约定自己的消息格式和含义。虽然,一个对象可以和将消息发送给任何对象,也可以接收任何对象发来的消息,但对象一般只处理和发送自己关心的消息。
在Windows中的窗口、任务和进程等对象间的信息沟通,都普遍采用这种消息机制。实际上,消息机制是Windows的基础之一。而DELPHI对象的消息处理机制一开始就是为了支持Windows消息而设计的,特别是用于窗口类的控件(即从TWinControl继承的控件)。但这种消息机制已经能够让所有的TObject对象采用这种方式通讯。如,我们熟悉的TLabel虽然不是一个窗口控件,但仍然能收到Windows发来的消息。当然,Windows是不会给一个TLabel发送消息的,那是DELPHI帮的忙。而TObject的Dispatch方法在这一个过程中起了关键性作用。
我们知道,DELPHI将Windows的消息描述为是一个联合结构,也叫变体结构。消息结构的第一个成员是一个四字节的整数,是区分消息类别的标识。其余的数据成员是根据消息类别的不同而有不同的定义。正是因为其余的成员是可以自由定义的,才使得消息处理机制有良好的扩展性。要知道,Windows有几千种不同类型的消息,DELPHI也自己扩展了若干种消息。随着软件版本的发展,消息的种类还会不断增加。
关心某种消息的对象类会为指定的消息定义一个消息处理方法,消息处理方法是用保留字message来声明的。例如:
TMouseObject = class(TObject)
public
procedure WMMouseMove(var Msg:TMessage); message WM_MOUSEMOVE;
procedure WMLButtonDown(var Msg:TMessage); message WM_LBUTTONDOWN;
end;
DELPHI的编译器将根据message保留字识别消息处理方法,并生成一个消息标识到该对象方法的映射表,连接到最终的执行程序中。事实上在DELPHI的内部,消息处理方法是用dynamic方法的机制实现的。前面我们说过,dynamic类型的方法是DELPHI的另一种虚方法,是可以重载以实现对象类的多态性。事实上,dynamic方法就是根据方法的序号找到调用地址的,这与根据消息ID找到各自的消息处理地址是没有什么本质区别的。因此,消息处理方法是可以由子类重载的,这可以让继承的对象实现自己的消息处理。不过,这种重载的语义与dynamic的重载有些不同。消息处理方法是按消息标识来重载的,即按message保留字后面的值。虽然,子类的消息处理方法的名称可以不同,只要消息标识相同即可实现重载。例如:
TNewMouseObject = class(TMouseObject)
public
procedure MouseMove(var Msg:TMessage); message WM_MOUSEMOVE;
procedure MouseDown(var Msg:TMessage); message WM_LBUTTONDOWN;
end;
其中,MouseMove方法重载了父类的WMMouseMove方法,而MouseDown重载了WMLButtonDown方法。当然,你也可以完全按dynamic的语义来定义重载:
TNewMouseObject = class(TMouseObject)
public
procedure WMMouseMove(var Msg:TMessage); override;
procedure WMLButtonDown(var Msg:TMessage); override;
end;
虽然,这没有任何错误,但我们很少这样写。这里只是要向大家说明message与dynamic的本质相同之处,以加深印象。
根据消息ID找到处理该消息的方法地址,就是所谓的"消息分发",或者叫"消息派遣",英文叫"Dispatch"。所以,TObject的Dispatch方法正是这个意思!只要你将消息传递给TObject的Dispatch方法,它将会正确地找到该消息的处理方法并交给其处理。如果,Dispatch方法找不到处理该消息的任何方法,就会调用DefaultHandler虚方法。虽然,TObject的DefaultHandler没有做任何事,但子类可以重载它以便自己处理漏网的消息。
Dispatch方法有一个唯一的参数Message,它是var的变量参数。这意味着可以通过Message参数返回一些有用的信息给调用者,实现信息的双向沟通。每一个消息处理方法都有一个唯一的参数,虽然参数类型是不一样的,但必须是var的变量参数。
值得注意的是,Dispatch的Message参数是没有类型的!
那么,是不是任何类型的变量都可以传递给对象的Dispatch方法呢?
答案是肯定的!
你可以将integer、double、boolean、string、variant、TObject、TClass……传递给一个对象的Dispatch方法,编译都不会出错。只不过DELPHI可能找不到这些东东对应的消息处理方法,即使碰巧找到,可能也是牛头不对马嘴,甚至产生运行错误。因为,Dispatch总是将Message参数的头4个字节作为消息ID到dynamic方法表中寻找调用地址的。
为什么DELPHI要这样定义Dispatch方法呢?
因为,消息类型是多种多样的,消息的大小和内容也是各不相同,所以只能将Dispatch方法的Message参数定义为无类型的。当然,DELPHI要求Message参数的头4个字节必须是消息的标识,但编译器并不检查这一要求。因为,这可能会扩充Object Pascal的语法定义,有些得不偿失,也许将来会解决这个问题。
通常,消息被定义为一个结构。这个结构的头4个字节被定义为消息标识,其余部分可以自由定义,大小随意。Windows的消息结构大小是固定的,但DELPHI可以允许定义任意大小的消息结构。虽然非Windows要求的固定大小消息结构可能无法用于Windows系统的消息传递,但对于我们在程序模块间定义自己的消息应用来说,却是非常方便的。
第六节 天苍苍,野茫茫
说了半天,我们已经了解到DELPHI原子世界的一个大概,也对DELPHI的最基础的一些东西有了一定的轮廓。这对于今后的学习和开发来说是非常有好处的,因为,我们毕竟知道了一些东西在内部是怎样实现的。
当然,还有许多东西我们还没有讨论,如类结构中的那些数据又指向什么地方?运行时刻信息(RTTI)又是什么结构?要把这些东东都讨论完,我们还需要进行更多的探索,恐怕最后的结果可以写一本厚厚的书。在今后的学习中,我们会再涉及到其中的内容。
真希望有一天我们能够彻底了解DELPHI原子世界的所有奥秘。但这似乎是不可能的,因为DELPHI还在不断发展,新的技术会不断的引入。因此,我们不追求最终的结果,探索的过程往往比最终的结果更幸福。
只要不断的努力,相信有一天,我们能上升到另一更高的思想境界。那时,我们将更加充实,世界在我们眼里将变得更美丽。虽然,天还是那样的蓝,大地还是那样的绿,但我们的心情又会怎样呢?
"天苍苍,野茫茫,风吹草低 见牛羊"