Delphi对于面向对象编程的支持丰富而且强大。除了传统的类和对象,Delphi还提供了接口,异常处理,多线程编程等特性。这一章节深入讲解了Delphi的对象模型。读者应当对标准的Pascal比较熟悉,并且对有关面向对象编程的基本法则有一定了解。
(本文的英文原文将Delphi与Object Pascal统一表述为Delphi,可能有概念不清之嫌疑。但在大多数情况下,相信读者能够根据上下文来判定文中所述之Delphi的具体含义——译者注。)
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方法。也就是说,永远不要尝试使用指向无效指针的变量,使用接口而不使用引用计数强制你必须这么做。