Delphi对象模型 (PART III)
Delphi对于面向对象编程的支持丰富而且强大。除了传统的类和对象,Delphi还提供了接口,异常处理,多线程编程等特性。这一章节深入讲解了Delphi的对象模型。读者应当对标准的Pascal比较熟悉,并且对有关面向对象编程的基本法则有一定了解。
(本文的英文原文将Delphi与Object Pascal统一表述为Delphi,可能有概念不清之嫌疑。但在大多数情况下,相信读者能够根据上下文来判定文中所述之Delphi的具体含义——译者注。)
构造器(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 <> nil then
Customer.ReleaseAccount(Self);
//调用TObject.Destroy.
inherited Destroy;
end;
const
DefaultInterestRate = 5000; // 5%, scaled by 1000
constructor TSavingsAccount.Create(Customer: TCustomer);
begin
//调用同类的另一个构造器
Create(Customer, DefaultInterestRate);
end;
constructor TSavingsAccount(Customer: TCustomer; InterestRate:Integer);
begin
//调用TAccount.Create
inherited Create(Customer);
fInterestRate := InterestRate;
end;
析构器(Destructor)
析构器和构造器一样也隐藏了一个附加的参数。第一次调用时,该附加参数被置为True。这使得Delphi调用FreeInstance来释放对象。如果该析构器调用了继承的析构器,那么Delphi将这个隐含的参数设置为False以防止继承的析构器再次释放同一个对象。
提示:
一个类通常有一个析构器名为Destroy。Delphi允许声明多个析构器——但这一特性并未带来什么 方便之处。定义多个析构器通常容易使人感到迷惑并且没有什么实际意义。
在Delphi执行析构器中的代码之前,它先调用虚方法BeforeDestruction。你可以覆盖该方法以确保在析构以前有些事务被处理掉。这个特性使你能写出安全的类代码,而不必担心派生类会在何时调用基类的析构器。
提示:
定义一个类时,你可能需要覆盖名为Destroy的析构器方法,但是不要重新定义Free方法。释放一个对象时,你要调用的是Free方法而不是析构器。这一区别非常重要,因为Free首先检查对象引用是否为nil,只有引用非空时才调用Destroy方法。只在某些特定的场合,才需要重新定义Free方法(比如很少用用到的单元VirtIntf中的TInterface类),因为可能调用Free比Destroy更重要。
假如构造器方法和AfterConstruction方法引发了异常, Delphi会自动调用析构器。写一个析构器时,必须意识到正在被撤销的对象有可能没有被完全的创建。Delphi确保所有的字段初始值为空,但假如在构造器中引发了异常,则可能导致某些字段已被初始化而有些未被初始化。如果构造器直接释放对象和指针,那么……其实不必担心这一点,因为Free方法和FreeMem过程都能自动检查指针是否为空。如果构造器调用其他方法,那么也会事先检查指针是否为空。