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 <> 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过程都能自动检查指针是否为空。如果构造器调用其他方法,那么也会事先检查指针是否为空。
Delphi之OOP对象模型Ⅳ
Interfaces接口
接口定义了包含一组抽象方法的类型。一个类,即使是自一个简单的基类继承而来也可以实现任意多的借口。接口与抽象类有些相似(即没有任何字段并且所有方法都是抽象方法的类),并且Delphi提供了附加的功能。Delphi的接口有时很象COM(组件对象模型)借口,但是使用Delphi的接口并不需要你了解有关COM的内容,同时你还可以将接口用作其他许多用途。
你可以声明一个新的接口--它继承于一个已经存在的接口。接口的声明包含了方法和属性的声明,但是没有字段。正如所有的类都继承于TObject一样,所有的接口类继承自IUnknown。接口IUnknown定义了三个方法:_AddRef,_Release,以及QueryInterface。如果你对COM熟悉的话,对此三个方法便不会陌生。前两个方法用于管理实现此接口的对象的生命周期引用计数。第三个方法用于存取对象可能实现的其他接口。
当你想要声明一个实现了一个或者多个接口的类时,你必须实现接口中所声明的所有方法。新的类可以直接实现接口的方法,也可以将此实现委托给一个属性--其值为一个接口。实现_AddRef,_Release以及QueryInterface方法最简单的方法就是继承TInterfacedObject及其派生类的方法,当然你也可以继承自其他类如果你想自己定以方法的实现的话。
新类在实现接口的方法时必须使用于接口方法一致的方法名,参数以及调用约定。Delphi自动将类的方法与接口的相应方法配对。假如要使用不同的方法名,你可以使用不同的方法名来重定向接口的方法。用作重定向的方法必须具有于接口的方法一致的参数和调用约定。这一特性非常重要,当一个类需要实现多个接口,而其中有重复的方法名时尤其如此。请在第五章查找关键字Class,以获得有关重定向方法的更多内容。
类可以使用implements指示符将接口的实现委托给一个属性。该属性的值必须得是该类将要实现的接口类型。当对象被映射到该接口上时,Delphi自动获取该属性的值,并且返回该接口。参考第五章中关于implements指示符的内容。
对于每个非委托方式实现的接口,编译器为其创建一个隐含的字段用于存放指向该接口的VMT。接口的字段正好位于对象隐含的VMT字段之后。正如对象引用其实是指向对象的隐含的VMT字段的指针,接口的引用也是指向隐含的VMT字段的一个指针。当对象被创建时Delphi自动初始化隐含字段。参考第三章有关编译器如何使用RTTI来追踪VMT和隐含字段的内容。
Reference counting引用计数
编译器触发对_AddRef和_Release的调用以管理接口对象的生命周期。要使用Delphid的自动的引用计数,声明一个接口类型的变量即可。当你将一个接口引用赋值给一个接口变量时,Delphi自动调用_AddRef。当改变量离开作用域时,Delphi自动调用_Release。
_ AddRef和_Release的行为完全取决于你。如果你从TInterfacedObject继承,则这些方法完成引用计数的功能。_AddRef方法用于增加引用计数,_Release用于将引用计数减一。当引用计数为0时,_Release方法将释放对象。如果你从其他类继承而来,则你可以定义自己的方法。但是,你应当正确的实现QueryInterface方法,因为Delphi正是基于此来实现As操作。
Typecasting类型转换
Delphi调用QueryInterface来对接口实现部分as操作的功能。你可以使用as操作符将一个接口转换为另外一个接口。Delphi调用QueryInterface以获得一个新的接口引用。如果QueryInterface返回一个错误,则as操作将触发一个运行期错误。(在SysUtils单元中该运行其错误被映射到EIntfCastError异常类中。)
你可以用自己的方式来实现QueryInterface方法,虽然可能你更倾向于与TInterfacedObject的实现接近的那种。例子 2-13 显示的是一个类实现了普通的QueryInterface方法,但是对于_AddRef和_Release方法的实现确大不相同。稍后你将看到这样做有什么用处。
例 2-13:无需引用计数的接口类
type
TNoRefCount = class(TObject, IUnknown)
protected
function QueryInterface(const IID:TGUID; out Obj):HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
function TNoRefCount.QueryInterface(const IID:TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := Windows.E_NoInterface;
end;
function TNoRefCount._AddRef: Integer;
begin
Result := -1
end;
function TNoRefCount._Release: Integer;
begin
Result := -1
end;
Interfaces and object-oriented programming接口和面向对象编程
接口最重要的作用就是将类型继承(type inheritance)与类继承(class inheritance)分开。类继承是代码重用的一项有效的工具。派生的类轻松的继承了基类的字段,方法以及属性,并且不需要重新实现公用的方法。在一个强类型的语言中,比如Delphi中,编译器将一个类看作是一种类型,因此类继承与类型继承的概念似乎有些重叠了。但是尽可能地,我们对于类型(type)和类(Class)还是应当严格区分。
许多有关面向对象编程的书籍都将继承关系描述为“是”的关系,比如,一个TSavingsAccount“是”TAccount。你可以体会到相同的含义,当你使用Delphi的Is操作符,来测试一个Account变量是否是TSavingsAccount。
上文例子中的简单的“是”关系已经不能适应要求。正方形属于矩形的一种,但这并不意味着你愿意将TSquare继承自TRectangle。矩形属于多边形的一种,但你可能不希望TRectangle继承自TPolygon。类继承强制派生的类保存基类中声明的所有字段,但这种情况下,派生类并不需要这些信息。一个TSquare对象只需保存它所有边的一个单一长度。然而,一个TRectangle对象却必须保存两个长度。一个TPolygon对象则需要保存许多条边和顶点位置。
解决的方案就是将其从类继承(类C继承了B的字段和方法,而B则继承了A的字段和方法)分离为类型继承(正方形是矩形,矩形又是多边形)。使用接口实现类型继承,则你可以让类继承做它擅长的:字段和方法的继承。
换句话说就是,ISquare继承自IRectangle,而后者又继承自IPolygon。接口遵从了“是”的关系。完全的与接口分离,类TSquare实现了接口ISquare和IRectangle和IPolygon。TRectangle实现了IRectangle和IPolygon。
提示:
COM编程的一个约定是将接口的名称命名为I打头的。Delphi的所有接口都遵循了这个约定。注意这只是一个有用的约定,并不是语言的强制要求。
从实现上而言,你可以声明符加的类以达到代码重用的目的。比如,使用TBaseShape实现对所有形状的公用字段和方法。TRectangle继承自TBaseShape然后实现跟根据矩形的特点实现相应方法。多边形依然继承自TBaseShape,并且根据多边形的特点实现相应的方法。
一个画图程序可以操作IPolygon接口来使用各种形状。例子 2-14显示的是基于这种设想的简单的类和接口。注意到每个接口都同时声明了GUID(全局唯一标识符)。使用QueryInterface时GUID是必须的。如果要使用接口的GUID,你可以直接使用接口的名称。Delphi会自动将接口的名称转换为对应的GUID。
例 2-14:分离类型和类继承
type
IShape = interface
[‘{50F6D851-F4EB-11D2-88AC-00104BCAC44B}‘]
procedure Draw(Canvas: TCanvas);
function GetPosition: TPoint;
procedure SetPosition(value: TPoint);
property Position: TPoint read GetPosition write SetPosition;
end;
IPolygon = interface(IShape)
[‘{50F6D852-F4EB-11D2-88AC-00104BCAC44B}‘]
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
IRectangle = interface(IPolygon)
[‘{50F6D853-F4EB-11D2-88AC-00104BCAC44B}‘]
end;
ISquare = interface(IRectangle)
[‘{50F6D854-F4EB-11D2-88AC-00104BCAC44B}‘]
function Side: Integer;
end;
TBaseShape = class(TNoRefCount, IShape)
private
fPosition: TPoint;
function GetPosition: TPoint;
procedure SetPosition(value: TPoint);
public
constructor Create; virtual;
procedure Draw(Canvas: TCanvas); virtual; abstract;
property Position: TPoint read fPosition write SetPosition;
end;
TPolygon = class(TBaseShape, IPolygon)
private
fVertices: array of TPoint;
public
procedure Draw(Canvas: TCanvas); override;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
TRectangle = class(TBaseShape, IPolygon, IRectangle)
private
fRect: TRect;
public
procedure Draw(Canvas: TCanvas); override;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
TSquare = class(TBaseShape, IPolygon, IRectangle, ISquare)
private
fSide: Integer;
public
procedure Draw(Canvas: TCanvas); override;
function Side: Integer;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
派生类继承了祖先类实现的接口。TRectangle继承自TBaseShape,则TBaseShape实现了IShape接口也就是TRectangle实现了IShape接口。而接口的继承与此有些不同。接口的继承仅仅为了类型上的便利,也就是说你不必重新再去输入许多方法的声明。当一个类实现一个接口时,并不意味着该类自动的实现了祖先的接口。事实上,该类只实现了出现在该类的声明部分的这些接口(以及在祖先类的声明部分出现的接口)。因此,即使IRectangle继承自IPolygon,TRectangle类还是得将IRectangle和IPolygon显式的罗列出来。
要实现类型体系,你不应当使用引用计数。相反,你需要实现显式的内存管理,如同处理普通的Delphi对象一样。在这种情况下,实现_AddRef和_Release 方法的最好办法就是连根拔除,就象我们在例 2-13里见到的TNoRefCount类那样。还有需要注意的是,不要有任何变量指向失效的引用。一个已经被释放的对象引用可能导致问题,因为Delphi将会自动调用_Release方法。也就是说,永远不要尝试使用指向无效指针的变量,使用接口而不使用引用计数强制你必须这么做。
Delphi之OOP对象模型Ⅴ
Object Life Cycle对象的生命周期
对于绝大多数对象,你调用构造器创建它,使用它,然后调用Free将它释放。Delphi替你处理了所有其他细节。或许,有时,你需要了解更多有关Delphi对象模型内部的实现机制。例2-8显示的是用于模拟Delphi创建和释放一个对象的方法。
例 2-8:对象的生命周期
type
TSomething = class
procedure DoSomething;
end;
var
Ref: TSomething;
begin
Ref := TSomething.Create;
Ref.DoSomething;
Ref.Free;
end;
//在构造器中被隐藏代码类似于这样:
function TSomething.Create(IsClassRef: Boolean): TSomething;
begin
if IsClassRef then
try
//获得新的对象实例
Self := TSomething.NewInstance;
//NewInstance初始化对象,功能与InitInstance类似。假如你覆盖了NewInstance,那么,
//不要调用inherited NewInstance,而应当调用InitInstance。
//这个调用正如下面所示,因此你可以知道究竟发生了什么,
//但是请记住通常Delphi实际上并不调用InitInstance。
InitInstance(Self);
//做构造器真正要做的工作,无需用到类引用。
//注意,Delphi并不真的对构造器进行递归调用。
Self.Create(False);
Self.AfterConstruction;
except
//若有任何异常发生,Delphi自动调用对象的析构器。
Self.Destroy;
end
else
Self.Create(False);
Result := Self;
end;
//构造器中被隐藏的代码类似于这样:
procedure TSomething.Destroy(Deallocate: Boolean);
begin
if Deallocate then
Self.BeforeDestruction;
// Delphi并不真的递归调用析构器,但这里是析构器真正发生作用的地方。
Self.Destroy(False);
if Deallocate then
begin
// Delphi并不真的调用CleanupInstance而是调用FreeInstance来做清理的工作。
// 假如覆盖了FreeInstance,不要调用inherited FreeInstance,而应当调用CleanupInstance
// 来清理字串、动态数组以及可变类型字段。
Self.CleanupInstance;
//调用FreeInstance来释放对象占用的内存。
Self.FreeInstance;
end;
end;
Access Levels访问级别
类似于C++和Java,Delphi提供了不同的访问级别控制机制,来决定一个对象是否能其他对象的字段,方法,以及属性等。访问级别分为以下几种:
private 私有的
私有的方法,只能被该类自己的方法以及在同一单元的实现部分中定义的方法、过程和函数访问。Delphi没有C++风格的友类声明也没有Java风格的包级别上的访问控制(package level access)。在Delphi中与之等同的是将包或者友类在同一单元声明,这样该单元中的所有类的私有及受保护部分都可以被访问。
protected 受保护的
受保护的方法,可以被该类以及派生类的任何方法访问。派生的类可以处于不同的单元中。
public 公用的
公开的方法没有访问限制。任何方法,函数,或者过程都可以访问公用声明的部分。除非使用$M+编译指示符,否则缺省的访问级别是公用的。
published发布的
发布的声明与公用的声明基本上相同,唯一的不同是Delphi为发布的声明保存运行时间信息。有些声明不能是发布的;详见第三章内容。如果类或者基类使用$M+指示符,则缺省的访问级别为发布的。
提示:
Delphi的IDE在Form最开始没有命名的部分声明字段和方法。因为TForm继承自TPersistent,而在TPersistent中使用了$M+指示符,因此最开始部分的访问级别为发布的published。换句话说,IDE将字段和方法声明为发布的。当Delphi装载一个Form描述文件(.dfm文件)时,它依赖于发布的信息来创建该Form对象。IDE依赖于Form类的最开始未命名部分的声明。假如你修改那一部分的东西,可能会有导致IDE的Form编辑器不可用的危险。
automated 自动的
自动的声明与公开的声明很接近,唯一的不同就是Delphi将保存更多的运行时间信息以支持OLE自动化服务。自动化的声明已经成陈旧了;你可以转而使用Delphi的类型库编辑器,但现在,他们为了向后兼容而保留了一部分。Delphi的下一版本可能会将这些部分彻底根除。第三章更进一步的阐述有关自动类型的声明。
派生的类可以提高属性的访问级别。通过在新的访问级别上重新声明属性来做到这一点(比如将首受保护的改为公开的)。但是不能降低一个属性的访问级别,并且你不能改变一个字段或者方法是否可见。你可以通过覆盖一个虚方法,或者声明一个覆盖方法在同样或者更高一级的访问级别上,但你不能降低访问级别。
Hiding a Constructor隐藏构造器
有时,一个类不用做公用的用途,而只是作为其他类的一个辅助类。这种情况下,你可能希望该辅助类的Create是私有的或者是受保护的,但这需要技巧。TObject声明了一个公开的构造器:Create.因此即使该辅助类的构造器是私有的或者受保护的,你依然可以调用自TObject继承而来的Create构造器。
尽管你不能改变继承来的Create构造器的访问权限,你还是可以通过另外一个公开的构造器来隐藏它。因为假如派生的构造器被调用,将引发一个异常。如下例:
type
TPublic = class;
TPrivateHelper = class
private
//TPublic是唯一一个被允许调用真正的构造器的类
constructor Create(Owner: TPublic);
overload;
public
//隐藏TObject.Create防止有人意外调用以试图创建TPrivateHelper的实例
constructor Create;
reintroduce; overload;
end;
TPublic = class
private
fHelper: TPrivateHelper;
public
constructor Create;
destructor Destroy;
end;
constructor TPrivateHelper.Create;
begin
raise Exception.Create(‘Programming error‘)
end;
constructor TPublic.Create;
begin
//这是唯一一个TPrivateHelper能被创建的地方
fHelper := TPrivateHelper.Create(Self);
end;
Properties属性
属性看起来更象是字段,但可以起到与方法一样的作用。属性替代了读取者和设置者(有时也称为getter和setter),但更加机动和强大。属性对于Delphi的IDE而言非常关键,同时,我们也可以在其他许多场合使用属性。
属性由一个读者和一个写者来负责读取和设置属性的值。读者(Reader)可以是一个字段名,一个集合字段的选择器,或是返回该属性值的一个方法。写者(writer)可以是一个字段名,一个集合字段的选择器或者可以设置该属性值的一个方法。你可以省略写者,那么该属性为只读属性。当然,也可以省略掉读者以创建一个只写的属性,但是使用这么一个怪怪的属性将很受限制。同时省略读者和写者是没有意义的,因此Delphi不允许你这么做。
大多数的读者和写者是字段名称或方法名称,你也可以将其引向部分集合字段(记录和数组)。如果一个读者或写者指向一个数组元素,那么数组的索引必须是常量,并且该字段不能为动态数组。纪录和数组可以嵌套,甚至你可以使用可变类型的记录。例2-9展示了一个扩展的矩形类型,与Windows的TRect类型相似,但它是一个类,有属性和方法。
例2-9:属性的读者与写者
TRectEx = class(TPersistent)
private
R: TRect;
function GetHeight: Integer;
function GetWidth: Integer;
procedure SetHeight(const value: Integer);
procedure SetWidth(const value: Integer);
public
constructor Create(const R: TRect); overload;
constructor Create(Left, Top, Right, Bottom: Integer); overload;
constructor Create(const TopLeft, BottomRight: TPoint); overload;
procedure Assign(Source: TPersistent); override;
procedure Inflate(X, Y: Integer);
procedure Intersect(const R: TRectEx);
function IsEmpty: Boolean;
function IsEqual(const R: TRectEx): Boolean;
procedure Offset(X, Y: Integer);
procedure Union(const R: TRectEx);
property TopLeft: TPoint read R.TopLeft write R.TopLeft;
property BottomRight: TPoint read R.BottomRight write R.BottomRight;
property Rect: TRect read R write R;
property Height: Integer read GetHeight write SetHeight;
property Width: Integer read GetWidth write SetWidth;
published
property Left: Integer read R.Left write R.Left default 0;
property Right: Integer read R.Right write R.Right default 0;
property Top: Integer read R.Top write R.Top default 0;
property Bottom: Integer read R.Bottom write R.Bottom default 0;
end;
Array properties数组型属性
数组型属性总是与数量有关,并且带有数组的特性。数组型属性不能被发布,但有许多其他用途。数组的索引可以是任何类型的,并且你可以使用多维的数组。对于数组型属性而言,你必须使用读者和写者的方法,因为你没有办法将一个数组型的属性直接映射到一个数组型的字段上。
可以将其中的一个数组型属性指定为默认的。则你可以直接使用对象引用以及一个数组标号来访问该项属性而无需指明属性名称,如例子 2-10所示。
例 2-10:使用缺省的数组属性
type
TExample = class
...
property Items[I: Integer]: Integer read GetItem write SetItem;
property Chars[C: Char]: Char read GetChar write SetChar; default;
end;
var
Example: TExample;
I: Integer;
C: Char;
begin
Example := TExample.Create;
I := Example.Items[4]; //必须显式的指明属性名称
C := Example[‘X‘]; //该数组型属性时缺省的
C := Example.Chars[‘X‘]; //效果如前
Delphi之OOP对象模型Ⅵ
Indexed properties索引型的属性
你可以将许多不同的属性映射到一个相同的读或写的方法,只需为每个属性指定一个索引值。这个索引值将被传递给读或写的方法以区别不同属性。
你甚至可以将数组型和索引型混合使用。读者和写者的方法将会区分他们--将数组的索引作为第一个参数,接着才是索引值。
Default values默认值
属性还用到stored和default两个指示符。这里的信息与 Delphi的Object Pascal语言的特性关系不大,但是Delphi的IDE将其用作保存Form的描述。stored指示符的值可以是一个Boolean类型的常量,也可以是一个Boolean型的字段,或是一个不需任何参数直接返回Boolean值方法。default指示符的值应当是与该属性的类型相同的一个常量。只有枚举型,整形以及集合类型的属性可以有一个缺省(default)的值。stored和default指示符只对发布的属性才有意义。
为了将缺省的数组数型与缺省的值区别开来,缺省数组的指示符以一个分号跟在属性的声明后面。缺省值指示符则直接置于属性声明以后。参见第五章了解有关default指示符的内容。
Using properties使用属性
通常情况下定义一个类时,我们将所有的字段设置为私有的,然后声明许多公开的属性来访问他们。然而Delphi并未对属性直接访问字段方式表示不满。但是使用属性,你可以在未来的某个时刻改变属性的实现,比如在字段值发生改变是增加校验等。你也可以使用属性来强制访问级别,比如当一个字段的之不能被改变时使用一个只读的属性。例 2-11显示的是声明和使用属性的几种方式。
例 2-11:声明和使用属性
type
TCustomer = record
Name: string;
TaxIDNumber: string[9];
end;
TAccount = class
private
fCustomer: TCustomer;
fBalance: Currency;
fNumber: Cardinal;
procedure SetBalance(NewBalance: Currency);
published
property Balance: Currency read fBalance write SetBalance;
property Number: Cardinal read fNumber; //不可改变
property CustName: string read fCustomer.Name;
end;
TSavingsAccount = class(TAccount)
private
fInterestRate: Integer;
published
property InterestRate: Integer read fInterestRate
write fInterestRate default DefaultInterestRate;
end;
TLinkedAccount = class(TObject)
private
fAccounts: array[0..1] of TAccount;
function GetAccount(Index: Integer): TAccount;
public
//两种属性访问数组的方法:使用索引或者引用一个数组元素
property Checking: TAccount index 0 read GetAccount;
property Savings: TAccount read fAccounts[1];
end;
TAccountList = class
private
fList: TList;
function GetAccount(Index: Integer): TAccount;
procedure SetAccount(Index: Integer; Account: TAccount);
function GetCount: Integer;
protected
property List: TList read fList;
public
property Count: Integer read GetCount;
property Accounts[Index: Integer]: TAccount read GetAccount
write SetAccount; default;
end;
procedure TAccount.SetBalance(NewBalance: Currency);
begin
if NewBalance < 0 then
raise EOverdrawnException.Create;
fBalance := NewBalance;
end;
function TLinkedAccount.GetAccount(Index: Integer): TAccount;
begin
Result := fAccounts[Index]
end;
function TAccountList.GetCount: Integer;
begin
Result := List.Count
end;
function TAccountList.GetAccount(Index: Integer): TAccount;
begin
Result := List[Index]
end;
procedure TAccountList.SetAccount(Index: Integer; Account: TAccount);
begin
fList[Index] := Account
end;
Class-type properties对象类型的属性
对象类型的属性需要引起格外的关注。使用对象类型的时候,最好由对象的拥有者负责管理对象属性。也就是说,单单保存一个对象引用是不够的,需要保留一分该对象属性的一个副本。是用一个写者方法来做到这一点。Delphi的IDE要求所有发布的属性满足这个要求,同时也对未发布的属性也产生影响。
此规则的唯一的例外是,属性保存的是对Form上的组件的引用。这种情况下,属性必须保存对象引用而非组件的副本。
Delphi的IDE只在.dfm中存放组件名称以保存组件引用的值。当.dfm被装载时,Delphi查找组件名以恢复对象引用的值。如果你必须要在一个组件内部保存一个完整的组件,则你必须实现对内部组件的属性的访问委托。
确认属性的类继承自TPersistent而来,并且该类覆盖了Assign方法。通过调用Assign来实现属性的写方法。(TPersistent,在Classes单元中定义,并不是必须的,但确是一个最简单的方法--复制一个对象。否则,你将花费两倍的代价在任何其他用到的类中书写Assigned方法。)读方法可以提供对字段的直接访问。如果该对象有一个OnChange的事件,你最好将设置其值以了解对象何时作了改变。例子 2-12显示了一个典型的使用对象属性的方法。例子中定义了一个图形控件,用于在需要时在其范围内以平铺的方式显示位图。属性Bitmap存放了一个TBitmap对象。
例 2-12:声明和使用对象类型的属性
unit Tile;
interface
uses SysUtils, Classes, Controls, Graphics;
type
// Tile a bitmap
TTile = class(TGraphicControl)
private
fBitmap: TBitmap;
procedure SetBitmap(NewBitmap: TBitmap);
procedure BitmapChanged(Sender: TObject);
protected
procedure Paint; override;
public
constructor Create(Owner: TComponent); override;
destructor Destroy; override;
published
property Align;
property Bitmap: TBitmap read fBitmap write SetBitmap;
property onClick;
property OnDblClick;
//还有许多有用的方法,限于空间不一一列出。详见TControl。
end;
implementation
{ TTile }
// Create the bitmap when creating the control.
constructor TTile.Create(Owner: TComponent);
begin
inherited;
fBitmap := TBitmap.Create;
fBitmap.OnChange := BitmapChanged;
end;
// Free the bitmap when destroying the control.
destructor TTile.Destroy;
begin
FreeAndNil(fBitmap);
inherited;
end;
// When the bitmap changes, redraw the control.
procedure TTile.BitmapChanged(Sender: TObject);
begin
Invalidate;
end;
// Paint the control by tiling the bitmap. If there is no
// bitmap, don‘t paint anything.
procedure TTile.Paint;
var X, Y: Integer;
begin
if (Bitmap.Width = 0) or (Bitmap.Height = 0) then
Exit;
Y := 0;
while Y < ClientHeight do
begin
X := 0;
while X < ClientWidth do
begin
Canvas.Draw(X, Y, Bitmap);
Inc(X, Bitmap.Width);
end;
Inc(Y, Bitmap.Height);
end;
end;
//通过复制TBitmap对象的方式设置新值
procedure TTile.SetBitmap(NewBitmap: TBitmap);
begin
fBitmap.Assign(NewBitmap);
end;
end.
(本文的英文原文将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 <> 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过程都能自动检查指针是否为空。如果构造器调用其他方法,那么也会事先检查指针是否为空。
Delphi之OOP对象模型Ⅳ
Interfaces接口
接口定义了包含一组抽象方法的类型。一个类,即使是自一个简单的基类继承而来也可以实现任意多的借口。接口与抽象类有些相似(即没有任何字段并且所有方法都是抽象方法的类),并且Delphi提供了附加的功能。Delphi的接口有时很象COM(组件对象模型)借口,但是使用Delphi的接口并不需要你了解有关COM的内容,同时你还可以将接口用作其他许多用途。
你可以声明一个新的接口--它继承于一个已经存在的接口。接口的声明包含了方法和属性的声明,但是没有字段。正如所有的类都继承于TObject一样,所有的接口类继承自IUnknown。接口IUnknown定义了三个方法:_AddRef,_Release,以及QueryInterface。如果你对COM熟悉的话,对此三个方法便不会陌生。前两个方法用于管理实现此接口的对象的生命周期引用计数。第三个方法用于存取对象可能实现的其他接口。
当你想要声明一个实现了一个或者多个接口的类时,你必须实现接口中所声明的所有方法。新的类可以直接实现接口的方法,也可以将此实现委托给一个属性--其值为一个接口。实现_AddRef,_Release以及QueryInterface方法最简单的方法就是继承TInterfacedObject及其派生类的方法,当然你也可以继承自其他类如果你想自己定以方法的实现的话。
新类在实现接口的方法时必须使用于接口方法一致的方法名,参数以及调用约定。Delphi自动将类的方法与接口的相应方法配对。假如要使用不同的方法名,你可以使用不同的方法名来重定向接口的方法。用作重定向的方法必须具有于接口的方法一致的参数和调用约定。这一特性非常重要,当一个类需要实现多个接口,而其中有重复的方法名时尤其如此。请在第五章查找关键字Class,以获得有关重定向方法的更多内容。
类可以使用implements指示符将接口的实现委托给一个属性。该属性的值必须得是该类将要实现的接口类型。当对象被映射到该接口上时,Delphi自动获取该属性的值,并且返回该接口。参考第五章中关于implements指示符的内容。
对于每个非委托方式实现的接口,编译器为其创建一个隐含的字段用于存放指向该接口的VMT。接口的字段正好位于对象隐含的VMT字段之后。正如对象引用其实是指向对象的隐含的VMT字段的指针,接口的引用也是指向隐含的VMT字段的一个指针。当对象被创建时Delphi自动初始化隐含字段。参考第三章有关编译器如何使用RTTI来追踪VMT和隐含字段的内容。
Reference counting引用计数
编译器触发对_AddRef和_Release的调用以管理接口对象的生命周期。要使用Delphid的自动的引用计数,声明一个接口类型的变量即可。当你将一个接口引用赋值给一个接口变量时,Delphi自动调用_AddRef。当改变量离开作用域时,Delphi自动调用_Release。
_ AddRef和_Release的行为完全取决于你。如果你从TInterfacedObject继承,则这些方法完成引用计数的功能。_AddRef方法用于增加引用计数,_Release用于将引用计数减一。当引用计数为0时,_Release方法将释放对象。如果你从其他类继承而来,则你可以定义自己的方法。但是,你应当正确的实现QueryInterface方法,因为Delphi正是基于此来实现As操作。
Typecasting类型转换
Delphi调用QueryInterface来对接口实现部分as操作的功能。你可以使用as操作符将一个接口转换为另外一个接口。Delphi调用QueryInterface以获得一个新的接口引用。如果QueryInterface返回一个错误,则as操作将触发一个运行期错误。(在SysUtils单元中该运行其错误被映射到EIntfCastError异常类中。)
你可以用自己的方式来实现QueryInterface方法,虽然可能你更倾向于与TInterfacedObject的实现接近的那种。例子 2-13 显示的是一个类实现了普通的QueryInterface方法,但是对于_AddRef和_Release方法的实现确大不相同。稍后你将看到这样做有什么用处。
例 2-13:无需引用计数的接口类
type
TNoRefCount = class(TObject, IUnknown)
protected
function QueryInterface(const IID:TGUID; out Obj):HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
function TNoRefCount.QueryInterface(const IID:TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := Windows.E_NoInterface;
end;
function TNoRefCount._AddRef: Integer;
begin
Result := -1
end;
function TNoRefCount._Release: Integer;
begin
Result := -1
end;
Interfaces and object-oriented programming接口和面向对象编程
接口最重要的作用就是将类型继承(type inheritance)与类继承(class inheritance)分开。类继承是代码重用的一项有效的工具。派生的类轻松的继承了基类的字段,方法以及属性,并且不需要重新实现公用的方法。在一个强类型的语言中,比如Delphi中,编译器将一个类看作是一种类型,因此类继承与类型继承的概念似乎有些重叠了。但是尽可能地,我们对于类型(type)和类(Class)还是应当严格区分。
许多有关面向对象编程的书籍都将继承关系描述为“是”的关系,比如,一个TSavingsAccount“是”TAccount。你可以体会到相同的含义,当你使用Delphi的Is操作符,来测试一个Account变量是否是TSavingsAccount。
上文例子中的简单的“是”关系已经不能适应要求。正方形属于矩形的一种,但这并不意味着你愿意将TSquare继承自TRectangle。矩形属于多边形的一种,但你可能不希望TRectangle继承自TPolygon。类继承强制派生的类保存基类中声明的所有字段,但这种情况下,派生类并不需要这些信息。一个TSquare对象只需保存它所有边的一个单一长度。然而,一个TRectangle对象却必须保存两个长度。一个TPolygon对象则需要保存许多条边和顶点位置。
解决的方案就是将其从类继承(类C继承了B的字段和方法,而B则继承了A的字段和方法)分离为类型继承(正方形是矩形,矩形又是多边形)。使用接口实现类型继承,则你可以让类继承做它擅长的:字段和方法的继承。
换句话说就是,ISquare继承自IRectangle,而后者又继承自IPolygon。接口遵从了“是”的关系。完全的与接口分离,类TSquare实现了接口ISquare和IRectangle和IPolygon。TRectangle实现了IRectangle和IPolygon。
提示:
COM编程的一个约定是将接口的名称命名为I打头的。Delphi的所有接口都遵循了这个约定。注意这只是一个有用的约定,并不是语言的强制要求。
从实现上而言,你可以声明符加的类以达到代码重用的目的。比如,使用TBaseShape实现对所有形状的公用字段和方法。TRectangle继承自TBaseShape然后实现跟根据矩形的特点实现相应方法。多边形依然继承自TBaseShape,并且根据多边形的特点实现相应的方法。
一个画图程序可以操作IPolygon接口来使用各种形状。例子 2-14显示的是基于这种设想的简单的类和接口。注意到每个接口都同时声明了GUID(全局唯一标识符)。使用QueryInterface时GUID是必须的。如果要使用接口的GUID,你可以直接使用接口的名称。Delphi会自动将接口的名称转换为对应的GUID。
例 2-14:分离类型和类继承
type
IShape = interface
[‘{50F6D851-F4EB-11D2-88AC-00104BCAC44B}‘]
procedure Draw(Canvas: TCanvas);
function GetPosition: TPoint;
procedure SetPosition(value: TPoint);
property Position: TPoint read GetPosition write SetPosition;
end;
IPolygon = interface(IShape)
[‘{50F6D852-F4EB-11D2-88AC-00104BCAC44B}‘]
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
IRectangle = interface(IPolygon)
[‘{50F6D853-F4EB-11D2-88AC-00104BCAC44B}‘]
end;
ISquare = interface(IRectangle)
[‘{50F6D854-F4EB-11D2-88AC-00104BCAC44B}‘]
function Side: Integer;
end;
TBaseShape = class(TNoRefCount, IShape)
private
fPosition: TPoint;
function GetPosition: TPoint;
procedure SetPosition(value: TPoint);
public
constructor Create; virtual;
procedure Draw(Canvas: TCanvas); virtual; abstract;
property Position: TPoint read fPosition write SetPosition;
end;
TPolygon = class(TBaseShape, IPolygon)
private
fVertices: array of TPoint;
public
procedure Draw(Canvas: TCanvas); override;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
TRectangle = class(TBaseShape, IPolygon, IRectangle)
private
fRect: TRect;
public
procedure Draw(Canvas: TCanvas); override;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
TSquare = class(TBaseShape, IPolygon, IRectangle, ISquare)
private
fSide: Integer;
public
procedure Draw(Canvas: TCanvas); override;
function Side: Integer;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
派生类继承了祖先类实现的接口。TRectangle继承自TBaseShape,则TBaseShape实现了IShape接口也就是TRectangle实现了IShape接口。而接口的继承与此有些不同。接口的继承仅仅为了类型上的便利,也就是说你不必重新再去输入许多方法的声明。当一个类实现一个接口时,并不意味着该类自动的实现了祖先的接口。事实上,该类只实现了出现在该类的声明部分的这些接口(以及在祖先类的声明部分出现的接口)。因此,即使IRectangle继承自IPolygon,TRectangle类还是得将IRectangle和IPolygon显式的罗列出来。
要实现类型体系,你不应当使用引用计数。相反,你需要实现显式的内存管理,如同处理普通的Delphi对象一样。在这种情况下,实现_AddRef和_Release 方法的最好办法就是连根拔除,就象我们在例 2-13里见到的TNoRefCount类那样。还有需要注意的是,不要有任何变量指向失效的引用。一个已经被释放的对象引用可能导致问题,因为Delphi将会自动调用_Release方法。也就是说,永远不要尝试使用指向无效指针的变量,使用接口而不使用引用计数强制你必须这么做。
Delphi之OOP对象模型Ⅴ
Object Life Cycle对象的生命周期
对于绝大多数对象,你调用构造器创建它,使用它,然后调用Free将它释放。Delphi替你处理了所有其他细节。或许,有时,你需要了解更多有关Delphi对象模型内部的实现机制。例2-8显示的是用于模拟Delphi创建和释放一个对象的方法。
例 2-8:对象的生命周期
type
TSomething = class
procedure DoSomething;
end;
var
Ref: TSomething;
begin
Ref := TSomething.Create;
Ref.DoSomething;
Ref.Free;
end;
//在构造器中被隐藏代码类似于这样:
function TSomething.Create(IsClassRef: Boolean): TSomething;
begin
if IsClassRef then
try
//获得新的对象实例
Self := TSomething.NewInstance;
//NewInstance初始化对象,功能与InitInstance类似。假如你覆盖了NewInstance,那么,
//不要调用inherited NewInstance,而应当调用InitInstance。
//这个调用正如下面所示,因此你可以知道究竟发生了什么,
//但是请记住通常Delphi实际上并不调用InitInstance。
InitInstance(Self);
//做构造器真正要做的工作,无需用到类引用。
//注意,Delphi并不真的对构造器进行递归调用。
Self.Create(False);
Self.AfterConstruction;
except
//若有任何异常发生,Delphi自动调用对象的析构器。
Self.Destroy;
end
else
Self.Create(False);
Result := Self;
end;
//构造器中被隐藏的代码类似于这样:
procedure TSomething.Destroy(Deallocate: Boolean);
begin
if Deallocate then
Self.BeforeDestruction;
// Delphi并不真的递归调用析构器,但这里是析构器真正发生作用的地方。
Self.Destroy(False);
if Deallocate then
begin
// Delphi并不真的调用CleanupInstance而是调用FreeInstance来做清理的工作。
// 假如覆盖了FreeInstance,不要调用inherited FreeInstance,而应当调用CleanupInstance
// 来清理字串、动态数组以及可变类型字段。
Self.CleanupInstance;
//调用FreeInstance来释放对象占用的内存。
Self.FreeInstance;
end;
end;
Access Levels访问级别
类似于C++和Java,Delphi提供了不同的访问级别控制机制,来决定一个对象是否能其他对象的字段,方法,以及属性等。访问级别分为以下几种:
private 私有的
私有的方法,只能被该类自己的方法以及在同一单元的实现部分中定义的方法、过程和函数访问。Delphi没有C++风格的友类声明也没有Java风格的包级别上的访问控制(package level access)。在Delphi中与之等同的是将包或者友类在同一单元声明,这样该单元中的所有类的私有及受保护部分都可以被访问。
protected 受保护的
受保护的方法,可以被该类以及派生类的任何方法访问。派生的类可以处于不同的单元中。
public 公用的
公开的方法没有访问限制。任何方法,函数,或者过程都可以访问公用声明的部分。除非使用$M+编译指示符,否则缺省的访问级别是公用的。
published发布的
发布的声明与公用的声明基本上相同,唯一的不同是Delphi为发布的声明保存运行时间信息。有些声明不能是发布的;详见第三章内容。如果类或者基类使用$M+指示符,则缺省的访问级别为发布的。
提示:
Delphi的IDE在Form最开始没有命名的部分声明字段和方法。因为TForm继承自TPersistent,而在TPersistent中使用了$M+指示符,因此最开始部分的访问级别为发布的published。换句话说,IDE将字段和方法声明为发布的。当Delphi装载一个Form描述文件(.dfm文件)时,它依赖于发布的信息来创建该Form对象。IDE依赖于Form类的最开始未命名部分的声明。假如你修改那一部分的东西,可能会有导致IDE的Form编辑器不可用的危险。
automated 自动的
自动的声明与公开的声明很接近,唯一的不同就是Delphi将保存更多的运行时间信息以支持OLE自动化服务。自动化的声明已经成陈旧了;你可以转而使用Delphi的类型库编辑器,但现在,他们为了向后兼容而保留了一部分。Delphi的下一版本可能会将这些部分彻底根除。第三章更进一步的阐述有关自动类型的声明。
派生的类可以提高属性的访问级别。通过在新的访问级别上重新声明属性来做到这一点(比如将首受保护的改为公开的)。但是不能降低一个属性的访问级别,并且你不能改变一个字段或者方法是否可见。你可以通过覆盖一个虚方法,或者声明一个覆盖方法在同样或者更高一级的访问级别上,但你不能降低访问级别。
Hiding a Constructor隐藏构造器
有时,一个类不用做公用的用途,而只是作为其他类的一个辅助类。这种情况下,你可能希望该辅助类的Create是私有的或者是受保护的,但这需要技巧。TObject声明了一个公开的构造器:Create.因此即使该辅助类的构造器是私有的或者受保护的,你依然可以调用自TObject继承而来的Create构造器。
尽管你不能改变继承来的Create构造器的访问权限,你还是可以通过另外一个公开的构造器来隐藏它。因为假如派生的构造器被调用,将引发一个异常。如下例:
type
TPublic = class;
TPrivateHelper = class
private
//TPublic是唯一一个被允许调用真正的构造器的类
constructor Create(Owner: TPublic);
overload;
public
//隐藏TObject.Create防止有人意外调用以试图创建TPrivateHelper的实例
constructor Create;
reintroduce; overload;
end;
TPublic = class
private
fHelper: TPrivateHelper;
public
constructor Create;
destructor Destroy;
end;
constructor TPrivateHelper.Create;
begin
raise Exception.Create(‘Programming error‘)
end;
constructor TPublic.Create;
begin
//这是唯一一个TPrivateHelper能被创建的地方
fHelper := TPrivateHelper.Create(Self);
end;
Properties属性
属性看起来更象是字段,但可以起到与方法一样的作用。属性替代了读取者和设置者(有时也称为getter和setter),但更加机动和强大。属性对于Delphi的IDE而言非常关键,同时,我们也可以在其他许多场合使用属性。
属性由一个读者和一个写者来负责读取和设置属性的值。读者(Reader)可以是一个字段名,一个集合字段的选择器,或是返回该属性值的一个方法。写者(writer)可以是一个字段名,一个集合字段的选择器或者可以设置该属性值的一个方法。你可以省略写者,那么该属性为只读属性。当然,也可以省略掉读者以创建一个只写的属性,但是使用这么一个怪怪的属性将很受限制。同时省略读者和写者是没有意义的,因此Delphi不允许你这么做。
大多数的读者和写者是字段名称或方法名称,你也可以将其引向部分集合字段(记录和数组)。如果一个读者或写者指向一个数组元素,那么数组的索引必须是常量,并且该字段不能为动态数组。纪录和数组可以嵌套,甚至你可以使用可变类型的记录。例2-9展示了一个扩展的矩形类型,与Windows的TRect类型相似,但它是一个类,有属性和方法。
例2-9:属性的读者与写者
TRectEx = class(TPersistent)
private
R: TRect;
function GetHeight: Integer;
function GetWidth: Integer;
procedure SetHeight(const value: Integer);
procedure SetWidth(const value: Integer);
public
constructor Create(const R: TRect); overload;
constructor Create(Left, Top, Right, Bottom: Integer); overload;
constructor Create(const TopLeft, BottomRight: TPoint); overload;
procedure Assign(Source: TPersistent); override;
procedure Inflate(X, Y: Integer);
procedure Intersect(const R: TRectEx);
function IsEmpty: Boolean;
function IsEqual(const R: TRectEx): Boolean;
procedure Offset(X, Y: Integer);
procedure Union(const R: TRectEx);
property TopLeft: TPoint read R.TopLeft write R.TopLeft;
property BottomRight: TPoint read R.BottomRight write R.BottomRight;
property Rect: TRect read R write R;
property Height: Integer read GetHeight write SetHeight;
property Width: Integer read GetWidth write SetWidth;
published
property Left: Integer read R.Left write R.Left default 0;
property Right: Integer read R.Right write R.Right default 0;
property Top: Integer read R.Top write R.Top default 0;
property Bottom: Integer read R.Bottom write R.Bottom default 0;
end;
Array properties数组型属性
数组型属性总是与数量有关,并且带有数组的特性。数组型属性不能被发布,但有许多其他用途。数组的索引可以是任何类型的,并且你可以使用多维的数组。对于数组型属性而言,你必须使用读者和写者的方法,因为你没有办法将一个数组型的属性直接映射到一个数组型的字段上。
可以将其中的一个数组型属性指定为默认的。则你可以直接使用对象引用以及一个数组标号来访问该项属性而无需指明属性名称,如例子 2-10所示。
例 2-10:使用缺省的数组属性
type
TExample = class
...
property Items[I: Integer]: Integer read GetItem write SetItem;
property Chars[C: Char]: Char read GetChar write SetChar; default;
end;
var
Example: TExample;
I: Integer;
C: Char;
begin
Example := TExample.Create;
I := Example.Items[4]; //必须显式的指明属性名称
C := Example[‘X‘]; //该数组型属性时缺省的
C := Example.Chars[‘X‘]; //效果如前
Delphi之OOP对象模型Ⅵ
Indexed properties索引型的属性
你可以将许多不同的属性映射到一个相同的读或写的方法,只需为每个属性指定一个索引值。这个索引值将被传递给读或写的方法以区别不同属性。
你甚至可以将数组型和索引型混合使用。读者和写者的方法将会区分他们--将数组的索引作为第一个参数,接着才是索引值。
Default values默认值
属性还用到stored和default两个指示符。这里的信息与 Delphi的Object Pascal语言的特性关系不大,但是Delphi的IDE将其用作保存Form的描述。stored指示符的值可以是一个Boolean类型的常量,也可以是一个Boolean型的字段,或是一个不需任何参数直接返回Boolean值方法。default指示符的值应当是与该属性的类型相同的一个常量。只有枚举型,整形以及集合类型的属性可以有一个缺省(default)的值。stored和default指示符只对发布的属性才有意义。
为了将缺省的数组数型与缺省的值区别开来,缺省数组的指示符以一个分号跟在属性的声明后面。缺省值指示符则直接置于属性声明以后。参见第五章了解有关default指示符的内容。
Using properties使用属性
通常情况下定义一个类时,我们将所有的字段设置为私有的,然后声明许多公开的属性来访问他们。然而Delphi并未对属性直接访问字段方式表示不满。但是使用属性,你可以在未来的某个时刻改变属性的实现,比如在字段值发生改变是增加校验等。你也可以使用属性来强制访问级别,比如当一个字段的之不能被改变时使用一个只读的属性。例 2-11显示的是声明和使用属性的几种方式。
例 2-11:声明和使用属性
type
TCustomer = record
Name: string;
TaxIDNumber: string[9];
end;
TAccount = class
private
fCustomer: TCustomer;
fBalance: Currency;
fNumber: Cardinal;
procedure SetBalance(NewBalance: Currency);
published
property Balance: Currency read fBalance write SetBalance;
property Number: Cardinal read fNumber; //不可改变
property CustName: string read fCustomer.Name;
end;
TSavingsAccount = class(TAccount)
private
fInterestRate: Integer;
published
property InterestRate: Integer read fInterestRate
write fInterestRate default DefaultInterestRate;
end;
TLinkedAccount = class(TObject)
private
fAccounts: array[0..1] of TAccount;
function GetAccount(Index: Integer): TAccount;
public
//两种属性访问数组的方法:使用索引或者引用一个数组元素
property Checking: TAccount index 0 read GetAccount;
property Savings: TAccount read fAccounts[1];
end;
TAccountList = class
private
fList: TList;
function GetAccount(Index: Integer): TAccount;
procedure SetAccount(Index: Integer; Account: TAccount);
function GetCount: Integer;
protected
property List: TList read fList;
public
property Count: Integer read GetCount;
property Accounts[Index: Integer]: TAccount read GetAccount
write SetAccount; default;
end;
procedure TAccount.SetBalance(NewBalance: Currency);
begin
if NewBalance < 0 then
raise EOverdrawnException.Create;
fBalance := NewBalance;
end;
function TLinkedAccount.GetAccount(Index: Integer): TAccount;
begin
Result := fAccounts[Index]
end;
function TAccountList.GetCount: Integer;
begin
Result := List.Count
end;
function TAccountList.GetAccount(Index: Integer): TAccount;
begin
Result := List[Index]
end;
procedure TAccountList.SetAccount(Index: Integer; Account: TAccount);
begin
fList[Index] := Account
end;
Class-type properties对象类型的属性
对象类型的属性需要引起格外的关注。使用对象类型的时候,最好由对象的拥有者负责管理对象属性。也就是说,单单保存一个对象引用是不够的,需要保留一分该对象属性的一个副本。是用一个写者方法来做到这一点。Delphi的IDE要求所有发布的属性满足这个要求,同时也对未发布的属性也产生影响。
此规则的唯一的例外是,属性保存的是对Form上的组件的引用。这种情况下,属性必须保存对象引用而非组件的副本。
Delphi的IDE只在.dfm中存放组件名称以保存组件引用的值。当.dfm被装载时,Delphi查找组件名以恢复对象引用的值。如果你必须要在一个组件内部保存一个完整的组件,则你必须实现对内部组件的属性的访问委托。
确认属性的类继承自TPersistent而来,并且该类覆盖了Assign方法。通过调用Assign来实现属性的写方法。(TPersistent,在Classes单元中定义,并不是必须的,但确是一个最简单的方法--复制一个对象。否则,你将花费两倍的代价在任何其他用到的类中书写Assigned方法。)读方法可以提供对字段的直接访问。如果该对象有一个OnChange的事件,你最好将设置其值以了解对象何时作了改变。例子 2-12显示了一个典型的使用对象属性的方法。例子中定义了一个图形控件,用于在需要时在其范围内以平铺的方式显示位图。属性Bitmap存放了一个TBitmap对象。
例 2-12:声明和使用对象类型的属性
unit Tile;
interface
uses SysUtils, Classes, Controls, Graphics;
type
// Tile a bitmap
TTile = class(TGraphicControl)
private
fBitmap: TBitmap;
procedure SetBitmap(NewBitmap: TBitmap);
procedure BitmapChanged(Sender: TObject);
protected
procedure Paint; override;
public
constructor Create(Owner: TComponent); override;
destructor Destroy; override;
published
property Align;
property Bitmap: TBitmap read fBitmap write SetBitmap;
property onClick;
property OnDblClick;
//还有许多有用的方法,限于空间不一一列出。详见TControl。
end;
implementation
{ TTile }
// Create the bitmap when creating the control.
constructor TTile.Create(Owner: TComponent);
begin
inherited;
fBitmap := TBitmap.Create;
fBitmap.OnChange := BitmapChanged;
end;
// Free the bitmap when destroying the control.
destructor TTile.Destroy;
begin
FreeAndNil(fBitmap);
inherited;
end;
// When the bitmap changes, redraw the control.
procedure TTile.BitmapChanged(Sender: TObject);
begin
Invalidate;
end;
// Paint the control by tiling the bitmap. If there is no
// bitmap, don‘t paint anything.
procedure TTile.Paint;
var X, Y: Integer;
begin
if (Bitmap.Width = 0) or (Bitmap.Height = 0) then
Exit;
Y := 0;
while Y < ClientHeight do
begin
X := 0;
while X < ClientWidth do
begin
Canvas.Draw(X, Y, Bitmap);
Inc(X, Bitmap.Width);
end;
Inc(Y, Bitmap.Height);
end;
end;
//通过复制TBitmap对象的方式设置新值
procedure TTile.SetBitmap(NewBitmap: TBitmap);
begin
fBitmap.Assign(NewBitmap);
end;
end.
loop's blog