Delphi剖析

一、前言

      关于你所提到VCL framework/Object pascal/windows SDK/Com,这些在Delphi的帮助都有提到,我想你之所以提到这些,是希望了解我对Delphi理解的深度和广度,基于这个认识,我在这里阐述一 下我对Delphi的认识。

      我对一个语言的掌握一般分三个步骤,不论是Delphi、Java、C/C++还是其他的:

      1、查看帮助或者相关资料,掌握其基本原理和架构,这个步骤主要还是以系统提供的帮助为主。

      2、学习源码,一个语言和工具的所有精髓都体现在他所提供的源码中。记得候捷曾经说过源码面前,无所遁形,对此深有同感。

      3、编写具体的应用,在编写过程中再不断地重复查看帮助和源码,在实践中加深对他的理解和把握。

      2004年的时候,朋友向我推荐了李维的《VCL架构剖析》,我到书店花了一个下午的时间扫了一遍,感觉写得很不错,确实写得很深入。你所提到的,以及 Delphi很多深层次的内容在这里都有相当精辟的分析。在最后我还是没有买下,因为他提到的绝大数自己都已经知道,或者正在这么做。

      对一个语言的掌握,我更倾向于掌握他的思路。任何语言之间相通的东西远远多于他本身的特性,因此我们在实现一个目标时,往往可以采取很多方式来实现。当这 些共性被掌握足够多的时候,任何一个新兴的语言和工具都可以举一反三,以此类推,当然,Java代码看起来C的情况还是可能发生的,^_^,但这并不会有 大的问题 。

二、Dephi架构概要

      先介绍下VCL的整体架构,这里介绍的和李维有不少相重复,但架构本来就只有一个,也无需特意回避什么。如果要详细地谈,估计要写本书了,所以还是以介绍 关键部分为主。这里介绍的是以Delphi 5为主,.Net没用过,也谈不出什么东西来。后面再详细介绍李维没有提到过的东西。

      1、Delphi 和 Object Pascal

      Delphi首先是开发工具,Object Pascal才是纯粹的语言。Delphi是Object Pascal在微软平台上的扩展,而Kylix是其在Posix(Linux、Unix)平台上的扩展,这是句废话,却反应了事物的本质。Delphi中 使用到技术和实现很多和微软平台中密切相关的,而Object Pascal却是他的本质。Delphi中的窗口、控件等的具体实现密切地应用微软平台的特性,因此只能局限在微软平台。

      2、Delphi和 API

      Delphi是在Windows API基础上封装的工具集、组件集,最大的特色是将过程性的API整合成对象的方法。因此Windows API中的内存、文件、图形三个方面的API在Delphi的VCL中有广泛的应用。

      在Delphi生成的每个工程中,都隐式包含SysInit.pas文件,这单元使用Object Pascal的外部函数导入语法,从“Kernel32.dll”中导入动态库管理函数、内存管理函数、线程变量(TLS)管理函数、命令行管理函数。可 以说,Object Pascal是父亲,Windows API是母亲,然后才有Delphi这个英雄般的儿子。SysInit做的工作,是初始化一个进程,以及进程结束时的扫尾工作。

      在一个单元中,即使没有Uses任何单元,但你依然可以使用System.pas接口中列出的方法、变量、类型,这告诉我们System.pas是被 IDE自动包含的。在System.pas中定义并实现了三个主要的类型,Tobject、Interface、变体。这三个部分在后面的内容有相当广泛 地应用。

      Classes.pas之后,才逐步脱离过程语言的影子,而更多体现出面向对象的优势。Tpersistent的功能其实很简单,就是为对象之间的拷贝而 设计的。关键是Tcomponent类,呵呵,我很喜欢研究这个类,感觉这个类比Tobject更象是Delphi的基类。所有的常用的组件和窗体都继承 自这个类。

      SysInit.pas 、 System.pas 、 Classes.pas 、 Control.pas 、 Forms.pas,这几个单元的引用关系充分体现了VCL构造的精髓。这里不得不提到一个有趣的现象,Controls.pas中引用了 Forms.pas单元,而Forms.pas中引用了Controls.pas,这种相互引用的现象在Delphi中很少见到,所以才觉得有趣。

      Tcanvas的设计又一次体现了对象封装API的强大功力。这个类的封装过程告诉我们一个事实,过程性语言和对象语言并不是完全对立的。在 Tcanvas所有的图形函数,都需要指示一个句柄,这个句柄就标识着同一个对象,而这就是Tcanvas成功实现的理论基础。在一个对象中,标识对象却 是对象指针,这和API是殊途同归的。

      3、Delphi和COM

      说老实话,我对COM这个架构还不是很认同。如果要实现一个服务的话,我更倾向于使用C/C++,通过TCP/IP监听请求,这样更容易具备跨平台的特 性。在高性能需求的应用上,微软平台的性能不敢恭维,我们公司现在做的服务都是在POSIX标准上实现。如果要做个控件的话,我更倾向于直接使用 Tcomponent实现组件。对COM的了解,更多的停留在使用层次上,当然也有少量的开发工作,因此不敢说有什么深刻的了解。

      COM更多解决的是在二进制级别上实现各种语言、工具之间的兼容。他仅仅提供一个接口上的标准,而把具体的实现留给了各个语言,接口定义语言(IDL)是 用于描述COM接口和方法的一种高级符号语言。COM组件通过一个或者多个相关函数集来存取组件的数据,这些函数集称为接口,而接口的函数称为方法。 COM组件通过接口指针调用接口的方法。

      在ComObj.pas单元中定义了Delphi中实现COM的基本结构,所有和COM相关的类都归于TcomObj和IUnknown。李维书中提道 “凡是支持Iunknown接口的都算是COM组件”,听起来感觉怎么这么象是林彪当年说的,却是一语中的。

      TcomObjFactory是为了更有效的管理TcomObj,如果套用设计模式所描述的模式,应该算是Factory模式吧,不过好像既不是抽象工 厂,也不是工厂方法模式,而应该是对象工厂模式吧,可惜《设计模式》这本书没有提到,呵呵,虽然叫工厂,感觉却接近于享元。当然 IclassFactory2是 License相关的。

      至于带有Type Info,具有Aggregation功能的,支持Idispatch的那些COM,只是在COM基础上的一个扩张,以此类推,由此及彼。Com Server可能有点另类,在TclientDataSet中,使用估计也是一种Com Server技术,因为我们在编译可执行的程序后,如果使用了这个数据集,就必须向系统注册Midas.dll,而Com Server就规定必须实现四个DLL函数。

      从个人理解来看,COM更像是在Delphi开发的控件上包了层壳,这个壳就是COM标准。虽然微软已经将COM标准提交给第三方组织管理,并成为公共的标准,但怎么看,都打上微软的标签。

       ComObj.pas和ActiveX.pas的关系,就像System.pas和Windows.pas的关系,换句话说,ComObj.pas是属于 Delphi的,而ActiveX.pas是属于Windows的。

       上面三点所概括的是对Delphi VCL架构的认识,下面有重点的谈些更深层次的理解。

三、一切归于汇编

      佛曰:“尘归尘,土归土”,在程序设计方面,也是类似的。不论是动态语言Java、Ruby,还是过程语言C、Pascal,甚至面向对象的C++、 Object Pascal,归根结底都要归结于汇编代码。设计语言经历着机器代码 à 汇编 à 过程语言 à 对象语言 à 动态语言 。不论什么语言,我们如果从汇编层次来看待这个问题的话,是很容易看出其中的最深刻的原理的。Object Pascal实际上是汇编+过程+对象的混合编程语言,下面我们探讨一下过程和对象在汇编层次的技术基础。

      1、数值计算

      过程语言的基础应用就是数值计算,不论后面的面向对象的语言还是更高层次的动态语言都没有放弃这个基本功能。

      2、函数调用

      在汇编语言中,已经存在着子过程的概念而调用子过程的就是Call方法,只不过过程语言使用函数这个更容易理解的方法来使用这个Call,而不是Push+Call,使用调用参数这个概念更容易接受。

      3、内存操作

      内存是如此的神奇,以至于在内存的操作能够演绎出多姿多彩的世界。过程语言、面向对象语言无一不是在内存上大做文章。C受人推崇的是指针,而受人诟病也同 样是指针,指针只是简单地指向内存中位置,如此简单,以至于无所不能,啥都能指。指针提供给开发人员强大的内存操作功能,同时也带来巨大的风险。就像剑有 双刃一样,既伤人也伤己,只有高手才能操控。

      4、结构与成员

      神说:世界要有光,于是这个世界就有了光。估计哪个程序员嫌对基础数据操作过于繁琐,于是结构就出现来拯救世界。结构的作用是将若干基础类型数据集中在一起,方便用户使用。

      结构的定义实际只是绑定了成员之间在内存中的相对内存位置,代码中对结构成员的访问实际上被转化对该成员在内存中与结构相对位置的访问,这个转化工作由编 译器自动完成,对开发人员来说,看到的依然是“结构·成员”这个访问方式。

      5、对象的实现原理

      对象的访问方式看起来和结构是如此的相似,以至于有很多文章介绍如何在C等过程语言中实现面向对象的编程。事实上,确实如此,结构的成员可以是函数指针,这和对象中对象方法是如此的相似。

      实际上,对象在语言中的实现和结构是异曲同工的。在Delphi中,对象实际上以结构的方式存放在内存中,VMT就是指示不同方法在该结构的相对位移。当 一个实例被创建的时候,Delphi就自动被为他分配一个InstanceSize大小的内存给他,并清空内存,然后初始化实例。

      对象之所以为对象,他和结构最根本的区别在于Self指针。我们在执行对象中方法时,如果查看EAX寄存器,会发现EAX中值和SELF是一样的。也就是 说,在一般情况下,Object Pascal总是将自身的指针放在EAX寄存器中,因此他总能找到自身。虽然结构和对象在表现上很相似,但是执行结构中的方法时,却无法找到自身,只能通 过外部传输句柄或者结构指针来获取自身的位置。

      “世间本来没有对象,因为有了编译器,于是就有对象”。呵呵,开个玩笑,但是归根结底来说,对象的生命是编译器赋予的,因为有了编译器,编程的生活才如此多姿多彩。

四、Tcomponent类

      Tobject的定义体现了Delphi关于对象设计的精华,但体现Delphi价值的却是Tcomponent 类。Delphi中关于操作系统消息的封装,更多的是为事件系统服务的。在Delphi中,事件更象是回调函数的一个子集,Delphi事件系统两个方 面,一个消息驱动的事件,另外一个是对象方法指针,如TnotifyEvent类型的事件。

      在Tcomponent类有个技术细节十分有趣,Notification是Tcomponent的一个保护的方法,但在Tcomponent. Notification方法中,有这么段代码:

      TComponent(FComponents[I]).Notification(AComponent, Operation);

      在面向对象的教程中,明确指出,对保护的方法只对自己和后代是可见的,而外部对象只能通过公共方法来访问。在这个例子中,显然调用的是外部对象的保护方 法,既然如此,那么有个问题就需要解决,这种与教条相违背的事情什么情况下允许发生呢?

      经过多次实验,发现当两个类处于同一个单元中,这两个类是可以互相访问保护甚至私有的方法和变量,这种方法是如此的有用,以至于通过这个方法可以实现一个 新的设计模式。比如存在这种情况,A类是B类的成员,当A类被创建时,能够自动在B类中注册自己,这个注册过程是自动完成;当然A类被析构时,也能够自动 在B类中注销自己,这个注销过程也是自动完成的。如果是使用前面提到的方法,可以我们可以轻而易举地将这个注册注销过程屏蔽起来。

五、动态和静态

      静态指的是在编译的时候就已经知道了,而动态指的是只有在运行时才知道。如果一个函数是静态的,那么对他的调用,一般直接使用Call该函数的地址,而动态方法却需要通过类的指针来计算该函数地址。

      Object Pascal是个过程和对象混合使用的语言,动态与静态之间的结合被应用的如此精妙,在Tobject中存在若干Class Function,这些函数实现的是对对象的操作,他们被绑定在类的定义中,而不是类的实例中,因此在Class Function中是无法引用类中非Class 方法的。这种方法在Object Pascal中显得有点古怪,以至于在除了Tobject的定义外,其他地方少有使用。

      在Java语言里,也专门使用Static保留字来处理这种情况,通过对象指针就可以直接访问的方法,而不需要创建新的实例来访问。这种现象说明了,即使 在自诩一切皆对象的Java语言中,静态的存在依然必不可少。

      对象的三大特性:封装、继承、多态中,封装利用的Self指针实现,而继承和多态却是由动态函数指针来实现的。

      Inherited关键字的使用可以很清晰的反应出继承关系的实现。假设我们先定义一个基类BaseClass,里面定义个虚函数vf,然后再实现一个子 类SubClass,覆盖该虚函数。如果在vf中使用Inherited,那么在Inherited的位置,会出现调用BaseClass中vf函数的代 码。但不管在什么地方调用vf函数,也不管SubClass是否覆盖vf虚函数,他的汇编代码总是Call [ebx + $000000e0],这意味着,vf函数在子类和基类中具有相同的占位符。只有在SubClass中使用Inherited这个关键字的时候,编译器才 会将其调用基类实际的函数。

      多态的实现,道理也是一样的,也是依靠动态的对象方法指针来实现的。

六、编译器就是上帝

      计算机象个洪荒时代,里面啥都没有,除了比特还是比特。就是因为有了编译器,才有如此丰富的语言和特性。在讨论VCL framework的时候,很多时候不得不提到编译器。比如面向对象的Self就是由编译器支持的,正是因为有了Self,开发人员才能在类方法中编写代 码,而无需考虑要访问的资源究竟是否是自己的。

你可能感兴趣的:(Delphi剖析)