Delphi之OOP面向对象模型 转
Delphi对于面向对象编程的支持丰富而且强大。除了传统的类和对象,Delphi还提供了接口,异常处理,多线程编程等特性。这一章节深入讲解了Delphi的对象模型。读者应当对标准的Pascal比较熟悉,并且对有关面向对象编程的基本法则有一定了解。
(本文的英文原文将Delphi与ObjectPascal统一表述为Delphi,可能有概念不清之嫌疑。但在大多数情况下,相信读者能够根据上下文来判定文中所述之Delphi的具体含义--译者注。)
Classesand 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与其他几种流行的编程语言的简要对比。
Table2-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
constructorCreate(Re: Double = 0.0); overload;
constructorCreate(Re, Im: Double); overload;
destructorDestroy; override;
procedureAssign(Source: TPersistent); override;
functionAsString: string;
published
propertyReal: 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。
ifAccount 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;
(本文的英文原文将Delphi与Object
Classes
为简短起见,术语“对象引用”简称为“对象”,但是对象更精确的表述应当是一块内存,Delphi在其中存放该对象的所有字段的值。在Delphi中使用一个对象的唯一方法就是使用对象引用。一个对象引用通常以一个变量的形式存在,但是也有函数或者属性返回值的形式。
例 2-1显示了几个类的声明。类的声明以关键字Class开头。类的声明中包含字段(field),方法(method),属性(property)等部 分,以关键字End结尾。每一个方法的声明类似于forword前导声明,你必须在同一单元中实现它(抽象abstract方法除外,有关抽象方法的内容 将在后面提到)。
type
TAccount
end;
TSavingsAccount
private
end;
TCheckingAccount
private
end;
TCertificateOfDeposit
private
end;
var
CD1,
begin
...
图2-1描述了例2-1中的对象和类在内存中的存放结构。变量以及相关对象存放于可读写的内存中。类存放在只读的内存中,与程序码放在一起。
Delphi的对象模型与其他几个面向对象语言的类似,比如C++和Java。表2-1显示了Delphi与其他几种流行的编程语言的简要对比。
Table
语言特性
继承
多重继承
接口
∨
单根类
元类
类(静态)字段
虚方法
抽象(纯)虚方法
类(静态)方法
动态方法
**回收
∨
可变类型
OLE自动化
静态类型校验
异常处理
函数(过程)重载
操作符重载
非类函数
非对象变量
属性
RTTI(运行期类型信息)
Generic类型(模板)
嵌入式线程支持
消息传递
嵌入式汇编
单行函数
我们将在以下几节中详细讨论这些语言特性。
类(Class)
类可以声明分为一个或多个部分,允许每一部分有不同的访问级别(可以是私有的private,受保护的 protected,公开的public,发布的published以及自动的automated等)。有关访问级别的内容将在后面谈及。你甚至可以将各 个声明部分任意排列,并且,允许相同的访问级别重复出现。
在声明的每一部分中,你可以定义任意多的字段,跟在方法和属性的声明后面。方法和属性的声明可以混在一起,但是在同一部分中所有字段必须声明在方法之前。与Java和C++不同,Delphi中不能在类声明中嵌套其他任何类型的声明。
类引用最通常的用法是调用该类的构造器constructor来创建一个对象实例。也可以使用类引用来检测对象的类型(使用Is操作符)。通常情况下,类引用是一个类名,但也可以是一个元类(metaclass)类型的变量,或者函数和属性的返回值。
type
TComplexClass
TComplex
private
public
constructor
constructor
destructor
procedure
function
published
property
property
end;
Delphi之OOP对象模型Ⅱ
对象(Object)
对象是类的一个动态的实例。这个动态实例包含了该类及其祖先类的所有字段。对象还包含一个隐含的字段用来保存对象所属类的一个类引用。
对象总是从堆中分配到内存,因此对象引用实际上是指向该对象的一个指针。程序设计人员负有在合适的时间创建和释放对象的责任。为了创建一个对象,我们使用类引用并调用构造器方法,如下例所示:
Obj
大 多数的构造器命名为Create,但这只是一个约定,并不是Delphi所一定要求的。有时你会发现其他名称的构造器,特别是在Delphi还不支持方法 的重载之前定义的一些陈旧的类。为了最大限度的与C++Builder保持兼容(因为C++Builder不允许自定义构造器名称),最好仍旧使用 Create,重载原先的构造器方法。
要删除程序中不再使用的一个对象,调用Free方法。为了确保即使在有异常触发的情况下,对象也能被正确释放,使用
Obj
try
Obj.DoSomethingThatMightRais
Obj.DoSomethingElse;
finally
Obj.Free;
end;
释 放一个全局的变量时,假如总是在释放对象后即将该变量置为nil,那么便不会留下一个包含无效指针的变量。释放对象之前而将对象置为nil一定得小心谨 慎。如果构造器或者构造器中调用的一个方法对该变量有一个引用,那么你最好将该变量置为nil以防可能的隐患。一个简单的方法是调用 FreeAndNill过程(在SysUtils中声明)。
GlobalVar
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
可 以使用类型转换以获得另一个类型的对象引用。类型转换并不改变对象;它只是给你一个新的对象引用。通常可以使用as操作符进行类型转换。as操作符自动检 查对象类型并且当对象的类并不是目标类的子类时将引发一个运行期错误。(SysUtils单元中将该运行期错误映射到EInvalidCast
另一种转换对象引用的方法是使用目标类的名称,类似函数调用。这种转换不会进行类型检查,因此只当你确信安全时才这么做。如例子2-3所示:
例2-3:使用静态的类型转换
var
Account: