Delphi对象模型 (PART II)
Delphi对于面向对象编程的支持丰富而且强大。除了传统的类和对象,Delphi还提供了接口,异常处理,多线程编程等特性。这一章节深入讲解了Delphi的对象模型。读者应当对标准的Pascal比较熟悉,并且对有关面向对象编程的基本法则有一定了解。
(本文的英文原文将Delphi与Object Pascal统一表述为Delphi,可能有概念不清之嫌疑。但在大多数情况下,相信读者能够根据上下文来判定文中所述之Delphi的具体含义——译者注。)
对象(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);//正确