Delphi 的类对象成员,System、TObject、TClass、对象的消息处理机制

Delphi 的类对象成员(System、TObject、TClass、对象的消息处理机制)

一、 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 语言程序编译之后不到 1K。

在 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 恐怕是唯一能实现跨平台编译功能的开发工具。

二、 TObject

TObject 是 System 单元中定义的第一个类。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 是 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 系统的消息传递,但对于我们在程序模块间定义自己的消息应用来说,却是非常方便的。