【转】Delphi类和组件-Delphi的原子世界

[摘要]本文介绍Delphi类和组件的相关知识,包括:System、TObject、TClass、对象的消息处理机制等。程序员

“天苍苍,野茫茫,风吹草低见牛羊”在使用 DELPHI 开发应用软件的过程当中,咱们就像草原上一群快乐牛羊,无忧无虑地享受着 Object Pascal 语言为咱们带来的温暖阳光和各类 VCL 控件提供的丰富水草。抬头望望一望无际蔚蓝的天空,低头品尝大地上茂密的青草,谁会去想天有多高?地有多大?阳光和水草又是从何而来?那是大师关心的事。而大师此时正坐在高高的山顶上,仰望宇宙星云变换,凝视地上小虫的爬行。蓦然回头,对咱们这群吃草的牛羊点头微笑。随手扯起一根小草,轻轻地含在嘴里,闭上眼睛细细品尝。不知道这根青草在大师的嘴里是什么味道?只是,他的脸上一直带着满意的微笑。编程

第一节 System

不经意,偶然打开了 System.pas 的原程序文件,却发现这里竟是一个既熟悉又陌生的世界。在这里有咱们熟知的东东,如:TObject、TClass、GUID、IUnknown、IDispatch ……但这些东西也是咱们所陌生的。在茫茫编程生涯中,咱们不断地与这些东东打交道,都已经熟悉得宛如本身身体的一部分。但真想要去了解他们,也就人象想要了解自身同样的茫然。安全

在 System.pas 单元的开头,有这样一段醒目的注释文本:数据结构

 

1
2
3
4
5
6
{ 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),它们并无实际的声明,而是编译器内置的,并在编译的开始就被认为是已经声明的定义”。ide

System 单元不一样于别的单元。你能够将 Classes.pas 或 Windows.pas 等其余 DELPHI 源程序文件加入你的项目文件中进行编译,并在源代码基础上调试这些单元。但你绝对没法将 System.pas 源程序文件加入到你的项目文件中编译!DELPHI 将报告“重复定义了 System 单元”的编译错误。函数

任何 DELPHI 的目标程序中,都自动包含 System 单元中的代码,哪怕你的程序一句代码也没写。看看下面的程序:工具

 

1
2
3
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 恐怕是惟一能实现跨平台编译功能的开发工具。ui

蜻蜓点水,浏览一下 DELPHI 的源代码是值得的。由于,DELPHI 的源代码中蕴藏着丰富的养分,那都是大师们的杰做。若是,咱们开发的应用应用程序是一棵开花的树,那么,请在咱们拥有这份花满枝丫的浪漫时,请不要忘了深埋在土壤里的那一藤树根。没有树根提供养分,就没有烂漫的花枝。要知道,世界上任何一棵树的树根总比其树冠更多,更茂盛,尽管人们看不到深埋在地下的树根。

但浏览 DELPHI 的源程序也是很费精力的。虽然,大师们写的程序大都风格一流,易于阅读和理解,但代码实在太多。阅读 System.pas 单元就更不容易,其中的大量程序甚至是用汇编语言编写的,这对有些朋友来讲无异于天书。咱们无心逐一去解读其中的奥秘,这可能会耗用咱们九九八十一个不眠之夜。但咱们总能学到一些编程风格,了解其中的一些内容,并能悟得一些道理,而这可能会让咱们受益终身。

固然,我无心将 DELPHI 的源代码神化为圣典。由于,那也毕竟不是天书,也是人编写的,也能抓到其中的几只臭虫。但咱们本身又怎样呢?

第二节 TObject

TObject 是什么?

TObject 在 DELPHI 中就是与生俱来的东西,没有什么好问的。

不知道 TObject 是什么,照样能够编写出很好的 DELPHI 程序。咱们能够当心苛护本身的 DELPHI 程序,“朝朝勤拂拭,莫让惹尘埃”,咱们的程序也能照样欢快地奔跑。世界上有不少的东西都是咱们不知道的,咱们同样也生活得很好。

但世上总有些人就是喜欢去学习和探索那些不知道的东西,最终他们知道的东西总比别人多些,成为了智者。我想,在编程中也是这样,若是通过咱们不断地学习和探索,将不知道的东西变成咱们知道的东西,咱们也会逐渐成为编程中的智者。相信总有一天能进入“原本无一物,何处惹尘埃”的境界。

TObject 是 System 单元中定义的第一个类。因而可知它在 DELPHI 中的重要性。TObject 的定义是这样的:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 中的对象是一个指针,这个指针指向该对象在内存中所占据的一块空间。

虽然,对象是一个指针,但是咱们引用对象的成员时却不能写成这样的代码:

 

1
MyObject^.GetName;

 

而只能写成:

 

1
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 数据的指针。

咱们再来看看对象是怎样诞生和灭亡的。咱们都知道,用下面的语句能够构造一个最简单对象:

 

1
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 对象就诞生了。

一样,用下面的语句能够消灭一个对象:

 

1
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 是这样定义的:

 

1
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 数据的理解。

有了类的类型,就能够将类做为变量来使用。能够将类的变量理解为一种特殊的对象,你能够象访问对象那样访问类变量的方法。例如:咱们来看看下面的程序片断:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的语句必定是非法的。

值得注意的是,构造函数是类方法,而析构函数是对象方法!

什么?构造函数是类方法,析构函数是对象方法!有没有搞错?

你看看,当你建立对象时分明使用的是相似于下面的语句:

 

1
aObject := TObject . Create;

 

分明是调用类 TObject 的 Create 方法。而删除对象时却用的下面的语句:

 

1
aObject.Destroy;

 

难道不是吗?TObject 是类,而 aObject 是对象。

缘由很简单,在构造对象以前,对象还不存在,只存在类,建立对象只能用类方法。相反,删除对象必定是删除已经存在的对象,是对象被释放,而不是类被释放。

最后,顺便讨论一下虚构造函数的问题。

在传统的 C++ 语言中,能够实现虚析构函数,但实现虚构造函数倒是一个难题。由于,在传统的 C++ 语言中,没有类的类型。全局对象的实例是在编译时就存在于全局数据空间中,函数的局部对象也是编译时就在堆栈空间中映射的实例。即便是动态建立的对象,也是用 new 操做符按固定的类结构在堆空间中分配的实例,而构造函数只是一个对已产生的对象实例进行初始化的对象方法而已。传统 C++ 语言没有真正的类方法,即便能够定义所谓静态的基于类的方法,其最终也被实现为一种特殊的全局函数。更不用说虚拟的类方法,虚方法只能针对具体的对象实例有效。所以,传统的 C++ 语言认为,在具体的对象实例产生以前,却要根据即将产生的对象构造对象自己,这是不可能的。的确不可能,由于这会在逻辑上产生自相矛盾的悖论!

然而,正是因为在 DELPHI 中有动态的类的类型信息,有真正虚拟的类方法,以及构造函数是基于类实现的等等这些关键概念,才可实现虚拟的构造函数。对象是由类产生的,对象就好象成长中的婴儿,而类就是它的母亲,婴儿本身的确不知道本身未来会成为何样的人,但是母亲们却用各自的教育方法培养出不一样的人,道理是相通的。

都知道强大的 VCL 是 DELPHI 得以成功的基础之一,而全部 VCL 的鼻祖是 TComponent 类。在 TComponent 类的定义中,构造函数 Create 被定义为虚拟的。这能使不一样类型的控件实现各自的构造方法,这就是 TClass 创造的类之类概念的伟大,也是 DELPHI 的伟大。

第四节 运用 TObject 的方法

咱们先来看看 TObject 的各个方法都是些什么东东。简要列示以下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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 的定义中,有两个方法值得咱们注意,就是:

 

1
2
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 来声明的。例如:

 

1
2
3
4
5
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 保留字后面的值。虽然,子类的消息处理方法的名称能够不一样,只要消息标识相同便可实现重载。例如:

 

1
2
3
4
5
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 的语义来定义重载:

 

1
2
3
4
5
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 还在不断发展,新的技术会不断的引入。所以,咱们不追求最终的结果,探索的过程每每比最终的结果更幸福。

只要不断的努力,相信有一天,咱们能上升到另外一更高的思想境界。那时,咱们将更加充实,世界在咱们眼里将变得更美丽。虽然,天仍是那样的蓝,大地仍是那样的绿,但咱们的心情又会怎样呢?

“天苍苍,野茫茫,风吹草低见牛羊”。

相关文章
相关标签/搜索