Delphi之OOP面向对向模型------『Delphi园地』

来源:互联网 发布:sqlserver数据仓库 编辑:程序博客网 时间:2024/06/05 17:36

======================================================
注:本文源代码点此下载
======================================================

delphi对于面向对象编程的支持丰富而且强大。除了传统的类和对象,delphi还提供了接口,异常处理,多线程编程等特性。这一章节深入讲解了delphi的对象模型。读者应当对标准的pascal比较熟悉,并且对有关面向对象编程的基本法则有一定了解。

(本文的英文原文将delphi与object pascal统一表述为delphi,可能有概念不清之嫌疑。但在大多数情况下,相信读者能够根据上下文来判定文中所述之delphi的具体含义--译者注。)

classes and objects类和对象

可以将类想象为一种特殊的记录。与记录相似的是,一个类描述的是一种特殊的类型,它由任意多个部分组成,每一个部分称为一个字段。与结构不同的是,一个类可以包含函数和过程(称为方法method),以及属性property。一个类可以继承自另一个类,因此,它继承了祖先类中所有的字段,方法,以及属性。

一个对象(object)是一个类的动态实例。对象总是从堆中动态分配得来,因此一个对象引用(object refrence)更象一个指针(但是不需要pascal的^操作符)。当你将一个对象引用赋值给一个变量时,delphi只是复制了指针,而不是复制整个对象实例。程序中不再结束使用一个对象时,应当调用free方法显式地释放该对象。delphi没有提供自动的**收集机制(后面一章中的提到的接口除外)。

为简短起见,术语“对象引用”简称为“对象”,但是对象更精确的表述应当是一块内存,delphi在其中存放该对象的所有字段的值。在delphi中使用一个对象的唯一方法就是使用对象引用。一个对象引用通常以一个变量的形式存在,但是也有函数或者属性返回值的形式。

类同样是一个独立的实体(与java中的类似,但与c++中的不同)。在delphi中,类表现为内存中一张只读的表,表中存放着指向该类的虚方法的指针以及其他许多信息。一个类引用(class reference)就是指向该表的一个指针。类引用最常见的用法是创建一个对象或者用来测试一个对象引用的类型,也可以在其它许多场合使用。比如,传递类引用给某个例程或者从一个函数中返回一个类引用。类引用的类型称为元类(metaclass)。

例2-1显示了几个类的声明。类的声明以关键字class开头。类的声明中包含字段(field),方法(method),属性(property)等部分,以关键字end结尾。每一个方法的声明类似于forword前导声明,你必须在同一单元中实现它(抽象abstract方法除外,有关抽象方法的内容将在后面提到)。

type

taccount = class

private

fcustomer: string; // name of customer

fnumber: cardinal; // account number

fbalance: currency; // current account balance

end;

tsavingsaccount = class(taccount)

private

finterestrate: integer; // annual percentage rate, scaled by 1000

end;

tcheckingaccount = class(taccount)

private

freturnchecks: boolean;

end;

tcertificateofdeposit = class(tsavingsaccount)

private

fterm: cardinal; // cd maturation term, in days

end;

var

cd1, cd2: taccount;

begin

cd1 := tcertificateofdeposit.create;

cd2 := tcertificateofdeposit.create;

...

图2-1描述了例2-1中的对象和类在内存中的存放结构。变量以及相关对象存放于可读写的内存中。类存放在只读的内存中,与程序码放在一起。

delphi的对象模型与其他几个面向对象语言的类似,比如c++和java。表2-1显示了delphi与其他几种流行的编程语言的简要对比。

table 2-1: delphi versus the world

语言特性 delphi java c++ visual basic

继承 ∨ ∨ ∨

多重继承∨

接口 ∨ ∨ [1]

单根类 ∨ ∨

元类 ∨ ∨

类(静态)字段∨ ∨

虚方法 ∨ ∨ ∨

抽象(纯)虚方法 ∨ ∨ ∨

类(静态)方法 ∨ ∨ ∨

动态方法 ∨

**回收 [2]

∨[2]

可变类型 ∨∨

ole自动化 ∨∨

静态类型校验 ∨ ∨ ∨

异常处理 ∨ ∨ ∨ ∨

函数(过程)重载 ∨ ∨ ∨

操作符重载∨

非类函数 ∨∨ ∨

非对象变量 ∨∨ ∨

属性 ∨∨

rtti(运行期类型信息) ∨ ∨ [3]

generic类型(模板)∨

嵌入式线程支持 ∨ ∨

消息传递 ∨

嵌入式汇编 ∨[4]

单行函数∨

我们将在以下几节中详细讨论这些语言特性。

类(class)

类的声明描述了该类所包含的字段(field),方法(method),以及属性(property)等信息。你可以在单元的interface或者implementation部分声明一个类,但是方法(method)--与函数或过程类似--必须得在implementation部分定义。同时,你必须在该类声明的同一单元内实现该方法。

类可以声明分为一个或多个部分,允许每一部分有不同的访问级别(可以是私有的private,受保护的protected,公开的public,发布的published以及自动的automated等)。有关访问级别的内容将在后面谈及。你甚至可以将各个声明部分任意排列,并且,允许相同的访问级别重复出现。

在声明的每一部分中,你可以定义任意多的字段,跟在方法和属性的声明后面。方法和属性的声明可以混在一起,但是在同一部分中所有字段必须声明在方法之前。与java和c++不同,delphi中不能在类声明中嵌套其他任何类型的声明。

类只有单一的基类,类从基类中继承所有字段,属性和方法。假如你不明确指明基类,delphi自动使用tobject作为基类。类可以实现任意多的接口。因而delphi的对象模型与java的极为类似,即一个类可以对一个简单的类进行扩展并且实现多重接口。

提示:

在delphi中有个约定,类型名称通常以字母t打头,如tobject。不过这只是一个约定而不是语法规则。并且,ide也通常以一个t开头来命名一个类。

类引用是对一个特定的类的一种引用。类引用并不是一个类对象(在jave和smalltalk中如此),但它可以用来创建新的对象,调用类方法,以及测试或试验对象的类型。类引用以指针的方式实现,这个指针指向有关该类信息的一张表,包括类的虚拟方法表(vmt)。(参见第三章“vmt中到底有些什么”相关内容。)

类引用最通常的用法是调用该类的构造器constructor来创建一个对象实例。也可以使用类引用来检测对象的类型(使用is操作符)。通常情况下,类引用是一个类名,但也可以是一个元类(metaclass)类型的变量,或者函数和属性的返回值。

例2-2:声明一个类以及元类

type

tcomplexclass = class of tcomplex; //元类类型

tcomplex = class(tpersistent)

private

freal, fimaginary: double;

public

constructor create(re: double = 0.0); overload;

constructor create(re, im: double); overload;

destructor destroy; override;

procedure assign(source: tpersistent); override;

function asstring: string;

published

property real: double read freal write freal;

property

end;

delphi之oop对象模型ⅱ

对象(object)

对象是类的一个动态的实例。这个动态实例包含了该类及其祖先类的所有字段。对象还包含一个隐含的字段用来保存对象所属类的一个类引用。

对象总是从堆中分配到内存,因此对象引用实际上是指向该对象的一个指针。程序设计人员负有在合适的时间创建和释放对象的责任。为了创建一个对象,我们使用类引用并调用构造器方法,如下例所示:

obj := tsomeclass.create;

大多数的构造器命名为create,但这只是一个约定,并不是delphi所一定要求的。有时你会发现其他名称的构造器,特别是在delphi还不支持方法的重载之前定义的一些陈旧的类。为了最大限度的与c++builder保持兼容(因为c++builder不允许自定义构造器名称),最好仍旧使用create,重载原先的构造器方法。

要删除程序中不再使用的一个对象,调用free方法。为了确保即使在有异常触发的情况下,对象也能被正确释放,使用 try-finally 异常处理。如下例所示:

obj := tsomeotherclass.create;

try

obj.dosomethingthatmightraiseanexception;

obj.dosomethingelse;

finally

obj.free;

end;

释放一个全局的变量时,假如总是在释放对象后即将该变量置为nil,那么便不会留下一个包含无效指针的变量。释放对象之前而将对象置为nil一定得小心谨慎。如果构造器或者构造器中调用的一个方法对该变量有一个引用,那么你最好将该变量置为nil以防可能的隐患。一个简单的方法是调用freeandnill过程(在sysutils中声明)。

globalvar := tfruitwigglies.create;

try

globalvar.eatemup;

finally

freeandnil(globalvar);

end;

每一个对象都包含它所有字段一个单独的副本。字段不能被多个对象所共享。如果确实需要共享一个字段变量,那么在单元层次上定义这个变量或者使用间接方法:在对象中使用指针或者对象引用来访问公共数据。

继承(inheritance)

一个类可以继承自另一个类。新派生的类继承了基类中所有的字段,方法以及属性。delphi只支持单一继承,因此派生类只有一个基类。而基类也可以有自己的基类,如此循环不断,一个类便继承了所有祖先类的字段,属性和方法。类还可以实现任意多的接口。类似于java,但c++不同的是,所有delphi的类都继承自同一个根类,那就是tobject。如果不显式的指明基类,delphi自动将tobject作为该类的基类。

提示:

类最直接的父类称为基类,这在类的声明中可以体现出来。类的祖先类可以是类的基类,也可以是一直到tobject的继承链中的其他祖先类。因而,在例子2-1中,类tcertificateofdeposit只有一个基类叫tsavingsaccount;而它的祖先类分别是tobject,taccount以及tsavingsaccount。

tobject类声明了一些方法以及一个特殊的,隐藏的字段专门用来存放对该对象所属类的引用。这个隐藏的字段指向类的虚拟方法表(vmt)。每一个类都有唯一的一个vmt并且所有该类的对象共用这个类的vmt。

可以将一个对象引用赋值给一个相同对象类型的,或者该类的任何一个祖先类的变量。换句话说,对象引用在声明时候的类型不一定要和实际的对象类型相同,反过来赋值--将一个基类的对象引用赋值给派生类的变量--是不允许的,因为对象可能会是不同的类型。

delphi保留了pascal的强类型校验特点,因此编译器根据一个对象引用声明时的类型对其进行检查。这样,要求所有的方法必须是类声明的一部分,并且编译器对函数和过程的变量也进行常规检查。编译器并不都将某个方法的调用绑定到特定的实现上。因为假如是一个虚方法,那么只有到运行时间时,才可以根据对象的真正的类型来决定哪个方法被调用。本章“方法”一节中详细说明了这个问题。

使用is操作符来测试对象所属的真正的类。当此类引用与对象的类相同或者此类引用是该对象类的一个祖先类时,返回true。当对象引用为nil或者不是该类,则返回false。

if account is tcheckingaccount then ... // tests the class of account if account is tobject then ... // true when account is not nil

可以使用类型转换以获得另一个类型的对象引用。类型转换并不改变对象;它只是给你一个新的对象引用。通常可以使用as操作符进行类型转换。as操作符自动检查对象类型并且当对象的类并不是目标类的子类时将引发一个运行期错误。(sysutils单元中将该运行期错误映射到einvalidcast 异常中。)

另一种转换对象引用的方法是使用目标类的名称,类似函数调用。这种转换不会进行类型检查,因此只当你确信安全时才这么做。如例子2-3所示:

例2-3:使用静态的类型转换

var

account: taccount;

checking: tcheckingaccount;

begin

account := checking; //允许

checking := account; // 编译错误

checking := account as tcheckingaccount; //没问题

account as tform; // 触发一个运行期错误

checking := tcheckingaccount(account); //可用,但不推荐

if account is tcheckingaccount then //更好的

checking := tcheckingaccount(account)

else

checking := nil;

字段(field)

字段是对象内部的变量。一个类可以声明任意多的字段,并且每一个对象都有自己的一个对自己类以及所有祖先类的所有字段的一个副本。或者说,字段可以称为一个数据成员,一个实例化的变量,或者一个特性。delphi没有提供类变量,类实例变量,静态数据成员或者等同的东西(即在同一类的所有对象中共享的变量)。但是你可以使用单元层次上的变量来达到类似的效果。

字段可以为任意类型除非是发布的(published)。在发布的声明部分中的字段必须要有运行时间类型信息。详见第三章内容。

在delphi中,新创建一个对象时,该对象的所有的字段被置空,也就是说,所有指针被初始化为nil,字符串以及动态数组的内容为空,数字值为0,布尔类型的值为false,并且可变类型variant的值被赋值为unassigned。

派生的类可以定义与祖先类中同名的字段。派生类的这个字段隐藏了祖先类中相同名称的字段。派生类中的方法引用的是派生类中的该字段,而祖先类的方法引用的是祖先类中的该字段。

方法(method)

方法是在类中实现的函数或者过程。c++中方法被称为“成员函数”。方法与普通的过程和函数的区别是,在方法中有一个隐含的参数称为self,用来指向调用该方法的对象本身。这里的self与c++和java中的相类似。调用一个方法与调用一个普通的函数或过程类似,但得将方法的名称跟在对象引用之后,如:

object.method(argument);

类方法(class method)基于类及其祖先类。在类方法中,self是对类的引用而不是对对象的引用。c++中类方法称为“静态成员函数”。

你可以调用在对象的类中以及祖先类里声明的对象方法。假如祖先类和派生类中定义了相同名称的方法,delphi将调用最外层派生的那个方法。如例2-4所示:

例2-4:绑定静态方法

type

taccount = class

public

procedure withdraw(amount: currency);

end;

tsavingsaccount = class(taccount)

public

procedure withdraw(amount: currency);

end;

var

savings: tsavingsaccount;

account: taccount;

begin

...

savings.withdraw(1000.00); //调用tsavingsaccount.withdraw

account.withdraw(1000.00); //调用taccount.withdraw

普通方法被称为静态方法的原因是编译器直接将该调用和方法的实现绑定在一起。换句话说,静态方法是静态绑定的。在c++中称普通方法被称为“普通成员函数”,在java中称为“最终方法(final method)”。多数delphi程序员不愿使用静态方法这个术语,而将之简化称为方法或者非虚拟方法。

虚方法是在运行期间而非编译期间被绑定的一类方法。在编译期间,delphi根据对象引用的类型来决定可以调用的方法。与编译期间直接指定一个特定的方法的实现不同的是,编译器根据对象的实际类型存放一个间接的对方法的引用。运行期间,程序在类的运行期表(特别是vmt)中查找方法,然后调用实际的类型的方法。对象的真正的类必须是在编译期中声明的类,或者它的一个派生的类--这一点不成问题,因为vmt提供了指向正确的方法的指针。

要声明一个虚方法,可以在基类中使用vritual指示符,然后使用override指示符以在派生的类中提供该方法的新的定义。与java不同的是,delphi中方法在缺省情况下是静态的,因此你必须使用virtual指示符来声明一个虚方法。与c++不同的是,delphi中要在派生类中覆盖一个虚方法必须使用override指示符。

例2-5 使用虚方法。

例2-5 绑定虚方法

type

taccount = class

public

procedure withdraw(amount: currency); virtual;

end;

tsavingsaccount = class(taccount)

public

procedure withdraw(amount: currency); override;

end;

var

savings: tsavingsaccount;

account: taccount;

begin

...

savings.withdraw(1000.00); // 调用tsavingsaccount.withdraw

account := savings;

account.withdraw(1000.00); // 调用tsavingsaccount.withdraw

除了vritual指示符,你还可以使用dynamic指示符。两者语义相同的,但实现不同。在vmt中查找一个虚方法很快,因为编译器在vmt中建了索引。而查找一个动态方法慢一些。调用一个动态方法虚要在动态方法表(dmt)中进行线性查找。在祖先类中查找直到遇到tobject或者该方法被找到为止。在某些场合,动态方法占用比虚方法更少的内存。除非要写一个vcl的替代物,否则你应当使用虚方法而不是动态方法。参见第三章以详细了解有关内容。

虚方法和动态方法可以在声明时使用abstract指示符,这样该类就不必给出对该方法的定义,但在派生的类中必须覆盖(override)该方法。c++中抽象方法的术语称为“纯虚方法”。当你调用一个包含有抽象方法的类的构造函数时, delphi将给出编译警告,提示你可能有个错误。可能你要创建的是覆盖(override)并且实现了该抽象方法的派生类的一个实例。定义了一个或者多个抽象方法的类通常称为抽象类,尽管有些人认定该术语只适用于只定义了抽象方法的那些类。

提示:

当你构建一个自其他抽象类继承而来的抽象类时,你应当使用override和abstract指示符将所有的抽象方法重新声明。delphi并没有要求这么做,因这只是个惯例。这些声明将清楚地告诉代码维护人员有哪些方法是抽象的。否则,维护人员可能对那些方法需要实现而那些方法需要保持抽象感到疑惑。例如:

type

tbaseabstract = class

procedure method; virtual; abstract;

end;

tderivedabstract = class(tbaseabsract)

procedure method; override; abstract;

end;

tconcrete = class(tderivedabstract)

procedure method; override;

end;

类方法或构造器也可以是虚拟的。在delphi中,类引用是一个真的实体,你可以将它赋值给一个变量,当作参数传递,或用作引用来调用类方法。如果构造器是虚拟的,则类引用有一个静态的基类类型,但你可以将一个派生类型的类引用赋值给它。delphi将在该类的vmt中查找虚拟构造器,而后调用派生类的构造器。,

方法(以及其他函数和过程)可以被重载,也就是说,多个例程可以有相同的名字,但是参数定义必须各不相同。声明重载方法使用overload指示符。在派生类中可以重载继承于基类的方法。这种情况下,只有派生的类才需要使用overload指示符。毕竟,基类的作者不可能预见其他的程序员何时需要重载一个继承的方法。如果派生类中没有使用overload指示符,则基类中的相同名称的方法被屏蔽。如例2-6所示。

例子2-6:方法的重载

type

tauditkind = (auinternal, auexternal, auirs, aunasty);

taccount = class

public

procedure audit;

end;

tcheckingaccount = class(taccount)

public

procedure audit(kind: tauditkind); // hides taccount.audit

end;

tsavingsaccount = class(taccount)

public

// can call tsavingsaccount.audit and taccount.audit

procedure audit(kind: tauditkind); overload;

end;

var

checking: tcheckingaccount;

savings: tsavingsaccount;

begin

checking := tcheckingaccount.create;

savings := tsavingsaccount.create;

checking.audit; // 错误,因为taccount.audit被屏蔽了。

savings.audit; //正确,因为audiot被重载了。

savings.audit(aunasty); //正确

checking.audit(auinternal);//正确

delphi之oop对象模型ⅲ

构造器(constructor)

每一个类都有一个或多个可能是自基类继承而来的构造器。按照惯例,构造器通常命名为create,但你也可以使用其他名称。有些构造器以create打头,为了传递更多的信息,被命名为诸如createfromfile或者createfromstream这样的名字。通常情况下,使用”create” 这个名字就可以了,因为你可以使用重载来定义多个相同名字的构造器。另一个原因是为了保持与c++builder的兼容。因为c++不允许构造器使用不同名称,因此你必须使用重载来定义多个构造器。

调用构造器

构造器是对象方法和类方法的混合体。你可以使用一个对象引用或者一个类引用来调用它。delphi会传递一个附加的隐含的参数来指示它如何被调用。假如使用一个类引用来调用构造器,delphi会调用类的newinstance方法以获得该类的一个新的实例。然后,构造器继续处理并且初始化对象。构造器自动引入一个try-except模块,当构造器中触发异常时,delphi将自动调用析构器。

使用对象引用来调用构造器时,delphi不会引入try-except块,也不会调用newinstance方法。相反,它象调用普通方法一样调用构造器。这个特性允许你调用继承的构造器而无需增加额外的内存开销。

提示:

一个常见的错误是尝试使用对象引用来创建一个对象,而不是用一个类引用来创建对象并将它赋值给一个对象引用:

var

account: tsavingsaccount;

begin

account.create; //错误

account := tsavingsaccount.create; //正确

delphi的特性之一是你可以控制在何时调用,如何调用,以及是否需要调用一个继承的构造器。这个特性使你可以构建功能强大的类,但在一定程度上也使得错误容易发生。

delphi总是先构造派生的类,仅当派生类调用了继承的构造器时才去构造基类。在c++中次序相反,从祖先类开始构建,最后才是派生的类。因而,假如有类c继承于b,而b继承于a,那么delphi先是构建c,然后是b最后是a.c++先构建a,然后b,最后c。

虚方法和构造器

另一个介于c++和delphi之间的一个很大的不同是,在c++中,构造器总是根据已经被创建的类的虚方法表来运行。而在delphi中,虚方法代表了所有派生类的内容,即使基类还没有被创建。因此,当你书写一个可能被构造器调用的虚方法时一定要小心。否则,对象可能还没有完全创建时该方法就被调用了。为了预防这种情况,你应当覆盖afterconstruction方法,在其中填写需要等到对象被完全创建后才能执行的代码。假如要覆盖afterconstruction,别忘了调用inherited方法。

一个构造器可以调用另一个构造器。delphi能够区分该调用是否来自于对象引用,因此调用构造器与调用普通方法相同。调用另一个构造器最常见的理由是把初始化代码放在一个单一的构造器中。例2-7显示了声明和调用构造器的几种不同的方法。

例2-7:声明和调用构造器

type

tcustomer = class ... end;

taccount = class

private

fbalance: currency;

fnumber: cardinal;

fcustomer: tcustomer;

public

constructor create(customer: tcustomer); virtual;

destructor destroy; override;

end;

tsavingsaccount = class(taccount)

private

finterestrate: integer; // scaled by 1000

public

constructor create(customer: tcustomer); override; overload;

constructor create(customer: tcustomer; interestrate: integer);

overload;

//注意:tsaveingaccount不需要再定义一个析构器。

//它只是简单的继承了taccount的构造器

end;

var

accountnumber: cardinal = 1;

constructor taccount.create(customer: tcustomer);

begin

inherited create; // call tobject.create.

fnumber := accountnumber; // assign a unique account number.

inc(accountnumber);

fcustomer := customer; // notify customer of new account.

customer.attachaccount(self);

end;

destructor taccount.destroy;

begin

//如果在设置fcustomer字段之前构造出错,则该字段为nil。

//仅当customer不为nil才释放account。

if customer


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/