yjmyzz:李战大师的成名,并不是因为08年发表于园子里的那篇"悟透javascript",而是多年前的这篇处女作"悟透delphi",原出处已经找不到了,近日重温delphi研究如何开发原生win32中的activex控件时,无意又找到了这篇文章,想当年这篇文章在delphi编程群体中那是何等轰动,转载于此,以示纪念。(delphi的出现,秒杀了vb/pb,vs的出现又秒杀了delphi,但是windows就其发展来看,不管如何发展,至少在今后相当长的时候间,也不可能彻底放弃win32的原生程序支持,所以存在即合理,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是不同的。类方 |
请发表评论