前不久,有位搞软件的朋友给我出了个谜语。谜面是“相亲”,让我猜一软件术语。我大约想了一分钟,猜
出谜底是“面向对象”。我觉得挺有趣,灵机一动想了一个谜语回敬他。谜面是“吻”,也让他猜一软件术
语。一分钟之后,他风趣地说:“你在面向你美丽的对象时,当然忍不住要和她接口!”。我们同时哈哈大
笑起来。谈笑间,似乎我们与自己的程序之间的感情又深了一层。对我们来说,软件就是生活。
第一节 接口的概念
“接口”一词的含义太广泛,容易引起误解。我们在这里所说的接口,不是讨论程序模块化设计中的程序接
口,更不是计算机硬件设备之间的接口。现在要说的接口,是一种类似于类的程序语言概念,也是实现分布
式对象软件的基础技术。
在DELPHI中,接口有着象类一样的定义方式,但不是用保留字class,而是用interface。虽然接口和类有着
相似的定义方式,但其概念的内涵却有很大的不同。
我们知道,类是对具有相同属性和行为的对象的抽象描述。类的描述是针对现实世界中的对象的。而接口不
描述对象,只描述行为。接口是针对行为方法的描述,而不管他实现这种行为方法的是对象还是别的什么东
西。因此,接口和类的出发点是不一样的,是在不同的角度看问题。
可以说,接口是在跨进程或分布式程序设计技术发展中,产生的一种纯技术的概念。类的概念是一种具有普
遍性的思想方法,是面向对象思想的核心。但是,接口概念也的确是伴随面向对象的软件思想发展起来的。
用接口的概念去理解和构造跨进程或分布式的软件结构,比起早期直接使用的远过程调用(RPC)等低级概念
更直观和简单。因为可以象理解一个对象一样理解一个接口,而不再关心这个对象是本地的或远程的。
在DELPHI中,接口被声明为interface。其命名原则是:接口都以字母I开头命名,正如类都以字母T开头一样
。在接口的声明中只能定义方法,而不能定义数据成员。因为,接口只是对方法和行为的描述,不存储对象
的属性状态。尽管在DELPHI中可以为接口定义属性,但这些属性必须是基于方法来存取的。
所有的接口都是直接或间接地从IUnknown 继承的。IUnknown是所有接口类型的原始祖先,有着类概念中
TObject的相同地位。“一个接口继承另一个接口”的说法其实是不对的,而因该说“一个接口扩充了另一个
接口”。接口的扩充体现的是一种“兼容性”,这种“兼容”是单一的,绝不会存在一个接口同时兼容两个
父接口的情况。
由于接口只描述了一组方法和行为,而实现这些方法和行为必须靠类。接口是不能创建实例的,根本就不存
在接口实例之说,只有类才能创建对象实例。但一个接口的背后一定会有一个对象实例,这个对象就是接口
方法的实现者,而接口是该对象一组方法的引用。
从概念上讲,一个对象的类可以实现一个或多个接口。类对接口的责任只是实现接口,而不应该说类继承了
一个或多个接口。“实现”一词和“继承”一词有不同的含义,应该从概念上区分开来。
一般情况下,声明接口时需要一个能唯一标识该接口类型的GUID标识符。接口类型是要被分布在不同进程空
间或计算机上的程序使用的,不象类的类型只是在一个程序空间内标识和使用。为了保证一种接口类型在任
何地方都能被唯一识别,就必须要一种有效标识不同接口的方法。用人工命名的方法是不行的,没有谁能保
证你开发的接口不会与别人重名。于是,一种所谓“全球唯一标识符”GUID(Globally Unique Identifier
)应运而生。它是通过一种复杂的算法随机产生的标识符,有16个字节长,可以保证全世界任和地方产生的
标识是不同的。在DELPHI的编辑环境中,你可以用Ctrl+Shift+G轻松产生一个GUID标识符,来作为接口的唯
一标识。
为接口指定GUID是必要的。虽然,不指定接口的GUID也可以常常可以编译通过,但在使用一些与接口识别和
转换相关的功能时一定会有问题。特别是在基于COM的程序开发中,GUID一定不可少。
接口的概念其实很简单,但却在分布式软件开发中起了关键作用。有的朋友之所以认为接口比较复杂,主要
是因为不了解接口的概念和原理。因为人们总是对自己未知的东西有一种神秘感。这种神秘感往往会使人对
未知世界产生畏惧心理。要揭开接口的神秘面纱,就必须不断的去学习和理解接口的奥秘。其实,在探索的
过程中还会有许多的乐趣,你说对吧。
第二节 IUnknown
因为IUnknown是所有接口的共同祖先,所以一定要首先了解它。知道事情的起因,可以有效地帮助我们理解
事情的过程和结果。IUnknown的原始定义是在System.pas单元中。因为定义在System.pas单元,那么一定是
与系统或编译器相关的原始东西。一看IUnknown的定义,很简单,一共才6行。
IUnknown = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
不过,这6行定义代码可是接口世界的基础。其中的三个接口方法蕴涵着简单而又博大精深的哲理,理解这些
哲理将使我们在编写基于接口的程序时受益非浅。
IUnknown的这三个接口方法是每一个接口对象类必须实现的方法,是接口机制的基础方法。为什么说这三个
方法是接口机制的基础?且听我漫漫道来。
首先来谈谈QueryInterface接口方法。我们知道一个对象类是可以实现多个接口的。任何接口对象都一定实
现了IUnknown接口。因此,只要你获得了一个接口指针,那一定可以通过这个接口指针调用QueryInterface
方法。而调用QueryInterface就可以知道这个接口指针还实现了一些什么接口。这对接口编程机制来说非常
重要。判断一个接口指针是否实现了某接口功能,不同接口类型之间的接口匹配和转换,都与
QueryInterface方法相关。
QueryInterface有两个参数和一个返回值。第一个参数是接口类型的标识,即一个16字节的GUID标识。由于
DELPHI编译器知道每种接口对应什么GUID,所以你可以直接使用象ImyInterface之类的标识符作为第一个参
数。如果,该接口支持第一个参数指定的接口类型,则将获得的接口指针通过第二个参数Obj送回给调用程序
,同时返回值为S_OK。
从这里也可以看出,为什么为接口指定GUID标识是必要的。因为,QueryInterface方法需要这样的标识,而
它又是接口和匹配和转换机制的基础。
接下来我们再谈谈_AddRef和_Release极口方法。_AddRef和_Release接口方法是每一种要接口对象类必须实
现的方法。_AddRef是增加对该接口对象的引用计数,而_Release是减少对接口对象的引用。如果接口对象的
引用计数为零,则要消灭该接口对象并释放空间。这本是接口机制要求的一个基本原则,就好象1+1=2这样简
单的道理,不需要深奥的解释。数学家才会有兴趣去研究一加一为什么会等于二?但数学家对1+1=2的理解是
透彻的。同样,对接口对象引用机制的深刻理解,会让我们明白许多道理,这些道理将为我们的开发工作带
来益处。
有位大师曾说过:接口是被计数的引用!
要理解这句话,我们首先要理解“引用”的概念。“引用”有“借用”的意思,表明一种参考关系。引用的
一方只存在找到被引用一方的联系,而被引用的一方才是真正的中心。由于,通过这种引用关系可以找到对
象,因此,引用实际就是该对象的身份代表。在程序设计中,引用实际上是一种指针,是用对象的地址作为
对象的身份代表。
在不是基于接口机制的程序中,本不需要就对象的引用关系进行管理。因为,非接口对象的实例都在同一个
进程空间中,是可以用程序严格控制对象的建立、使用和释放过程的。可是,在基于接口机制的程序中,对
象的建立、使用和释放可能出现在同一进程空间中,也可能出现在不同的进程空间,甚至是Internet上相隔
千里的两台计算机中。在一个地方建立一个接口,可能实现这个接口的对象又存在于另一个地方;一个接口
在一个地方建立后,又可能会在另一个地方被使用。在这种情况下,要想使用传统的程序来控制对象的建立
和释放就显得非常困难。必须要又一种约定的机制来处理对象的建立和释放。因此,这一重任就落到了
IUnknown的_AddRef和_Release的身上。
这种接口对象引用机制要求,接口对象的建立和释放由对象实例所在的程序负责,也就是由实现接口的对象
类负责。任何地方引用该对象的接口时,必须调用接口的_AddRef方法。不再引用该对象时,也必须调用接口
的_Release方法。对象实例一旦发现再也没有被任何地方引用时,就释放自己。
正是为了解决接口对象实例空间管理的问题,_AddRef和_Release方法才成为所有接口对象类必须实现的方法
。
第三节 接口对象的生死
初看本节的标题似乎有点吓人。接口对象怎么会和生与死联系起来呢?接口对象的生死真的那么重要吗?一
个好的统治者应该关心百姓的生死,同样,一个好的程序员也应该关心对象的生死。而接口对象又是流浪在
分布式网络中的游子,我们更应该关心他们的生死!
由于,接口对象是伴随接口引用的产生而建立,又伴随接口引用的完结而消亡。在DELPHI 中使用接口,似乎
没有人关心,实现接口的对象是怎样出身又怎样死亡的。这正是DELPHI中使用接口的简单性,也是其在解决
接口机制的使用问题上所追求的目标。需要接口时总有一个对象会为她而生,一旦不再引用任何接口时这个
对象又无怨无艾的死去,绝不拖累系统一个字节的资源。真有点“春蚕到死丝方尽,蜡炬成灰泪始干”的凄
情。
因为接口对象的生死直接和引用该对象的接口数目有关,所以研究在什么情况下会增加一次接口引用,又在
什么情况下会减少一次接口引用,是了解接口对象生死的关键。
现在我们来实现一个最简单的接口对象类TIntfObj,它只实现了IUnknown接口中定义的三个基本方法。有的
朋友一看就知道,这个类实际抄袭了DELPHI中TInterfacedObject类的部分代码。只是我们分别在_AddRef和
_Release方法中增加了一些信息输出语句,以便于我们探索接口对象的生死问题。请看下面的程序:
program ProgramA;
uses
SysUtils, Dialogs;
type
TIntfObj = class(TObject, IUnknown)
protected
FRefCount: Integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
function TIntfObj.QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
const
E_NOINTERFACE = HResult($80004002);
begin
if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE;
end;
function TIntfObj._AddRef: Integer; stdcall;
begin
INC(FRefCount);
ShowMessage(Format('Increase reference count to %d.', [FRefCount]));
result:=FRefCount;
end;
function TIntfObj._Release: Integer; stdcall;
begin
DEC(FRefCount);
if FRefCount <> 0 then
ShowMessage(Format('Decrease reference count to %d.', [FRefCount]))
else begin
Destroy;
ShowMessage('Decrease reference count to 0, and destroy the object.');
end;
result:=FRefCount;
end;
var
aObject:TIntfObj;
aInterface:IUnknown;
procedure IntfObjLife;
begin
aObject:=TIntfObj.Create;
aInterface:=aObject; //增加一次引用
aInterface:=nil; //减少一次引用
end;
begin
IntfObjLife;
end.
我们需要用单步调试功能来研究接口引用计数的增减与接口生死的关系。因此,建议你将Options选项中
Complier页的Optimization项清除,以避免编译器会优化掉我们需要的指令。
当程序执行到IntfObjLife子程序的三行代码时,请一步一步的调试代码。你会发现,当发生一次对接口类型
变量的赋值行为时,就会引发对接口引用计数的增减。
执行语句
aInterface:=aObject;
就会出现“Reference count increase to 1.”的信息,表明增加了一次接口引用。
而执行语句
aInterface:=nil;
就会出现“Reference count decrease to 0, and destroy the object.”,表明接口引用减少到零并且删
除了接口对象。
所以,我们可以得出结论:当将引用值赋值给接口类型的变量时,会增加对接口对象的引用计数;而当清除
接口类型变量的引用值时(赋nil),就会减少对接口对象的引用计数。
在看看下面的代码,加深一下对这一结论的理解。
var
aObject : TIntfObj;
InterfaceA, InterfaceB : IUnknown;
……
aObject := TIntfObj.Create;
InterfaceA := aObject; //引用增加至1
InterfaceA := InterfaceA; //引用增加至2,但立即又减少至1
InterfaceB := InterfaceA; //引用增加至2
InterfaceA := nil; //引用减少至1
InterfaceB := InterfaceA; //引用减少至0,释放对象
……
无论是将接口对象赋值给变量,还是将接口变量赋值给接口变量,以及将nil赋值给接口变量,都印证这一结
论。有趣的是,其中的InterfaceA := InterfaceA一句执行时,接口对象的引用先增加然后立即减少。为什
么会这样呢?留给你自己去思考吧!
接着,我们再来看看下面的代码:
procedure IntfObjLife;
var
aObject:TIntfObj;
aInterface:IUnknown;
begin
aObject:=TIntfObj.Create;
aInterface:=aObject;
end;
这个过程与前面那个过程不同的是,将变量定义为局部变量,并且最后没有给接口变量赋nil值。单步调试这
段代码我们发现,在程序运行到子程序end语句之前,接口对象的引用还是减少为0并被释放。这是为什么呢
?
我们知道,变量是有作用域的。全局变量的作用域是程序的任何地方,而局部变量的作用域只是在相应的子
程序内。一旦变量离开它的作用域,变量本身已经不存在,它存储的值更是失去意义了。所以,当程序即将
离开子程序时,局部变量aInterface将不在存在,而其所存储的接口对象引用值也将失去意义。聪明的
DELPHI自动减少对接口对象的引用计数,以确保程序在层层调用和返回中都能正确地管理接口对象的内存空
间。
因此,我们又可以得出新的结论:当任何接口变量超出其作用域范围的时候,都会减少相关接口对象的引用
计数。
要注意的是,子程序的参数变量也是一种变量,它的作用域也是在该子程序范围内。调用一个含有接口类型
参数的子程序时,由于参数的传递,相关接口对象的引用计数会被增加,子程序返回时又被减少。
同样,如果子程序的反回值是接口类型时,返回值的作用域是从主调程序的返回点开始,直到主调程序最后
的end语句之前的范围。这种情况同样会引起对接口对象引用计数的增减。
该总结一下对象的生与死了。盖棺论定后我们可以得出以下原则:
1. 将接口对象的引用值赋值给全局变量、局部变量、参数变量和返回值等元素时,一定会增加接口对象的
引用计数。
2. 变量原来存储的接口引用值被更改之前,将减少其关联对象的引用计数。将nil赋值给变量是一个赋值和
修改接口引用的特例,它只减少原来接口对象引用计数,不涉及新接口引用。
3. 存储接口引用值的全局变量、局部变量、参数变量和返回值等元素,超出其作用域范围时,将自动减少
接口对象的引用计数。
4. 接口对象的引用计数为零时,自动释放接口对象的内存空间。(在一些采用了对象缓存技术的中间件系
统中,如MTS,可能并不遵循这一原则)
需要提醒你的是,一旦你将建立的接口对象交给接口,对象的生死就托付给接口了。就好象将宝贝女儿嫁给
忠厚的男人一样,你应该完全信任他,相信他能照顾好她。从此,与对象的联系都要通过接口,而不要直接
与对象打交道。要知道,绕过女婿直接干预女儿的事情有可能会出大问题的。不信,我们看看下面的代码:
program HusbandOfWife;
type
IHusband = interface
function GetSomething:string;
end;
TWife = class(TInterfacedObject, IHusband)
private
FSomething:string;
public
constructor Create(Something:string);
function GetSomething:string;
end;
constructor TWife.Create(Something:string);
begin
inherited Create;
FSomething:=Something;
end;
function TWife.GetSomething:string;
begin
result := FSomething;
end;
procedure HusbandDoing(aHusband:IHusband);
begin
end;
var
TheWife : TWife;
TheHusband : IHusband;
begin
TheWife := TWife.Create('万贯家财');
TheHusband := TheWife; //对象TheWife委托给一般接口变量TheHusband
TheHusband := nil; //清除接口引用,对象消失
TheWife.GetSomething; //直接访问对象,一定出错!
TheWife := TWife.Create('万贯家财');
HusbandDoing(TheWife); //对象委托给参数接口变量aHusband,返回时对象消失
TheWife.GetSomething; //直接访问对象,一定出错!
end.
请大家仔细看最后面begin至end之间的代码。我尽量将程序写的有趣和易于理解,希望你能读懂我的程序。
其基本意思是,产生一个TheWife对象之后,一旦将该对象传递给一个IHusband类型的接口后,再使用
TheWife直接操纵对象就可能发生想象不到的问题!那“万贯家财”给出去容易,拿回来可就难哟。
所以,在进行基于接口的程序设计中,请切记:接口对象一旦建立,请永远用接口来操纵对象!
现在,你该认为本节的题目并不吓人了吧!如果是这样,我写本节的目的就达到了。我也因为你能静静地倾
听我诉说衷肠而感到心情舒畅。那么,谢谢你!让我们共同在愉快的心情中继续前进吧。
第四节 接口方法的背后
俗话说,每一个成功的男人背后,一定有一个了不起的女人。同样,每一个接口背后一定有一个了不起的对
象。今天我们将了解一下接口是如何映射到对象而实现接口功能 的。
我们都知道,一个接口值实际上是一个指针。这个指针指向一张方法地址表,其中地址表的每一项存有真正
要调用的对象方法的地址。通过接口值调用方法时,就可以通过其指向的方法地址表中映射的对象方法地址
找到对象的方法,从而实现正确的方法调用。
这样理解接口到对象的方法映射关系本没有什么不正确,但却忽略了怎样通过接口只找到其引用的对象的问
题。因为,一个接口值不仅代表了实现的一组方法,而且还表明这个接口值的方法是由其引用的那个对象实
现的,而不是由另一个对象实现的(尽管是同一类的对象)。
所以,对接口值的调用首先是定位对象实例,然后才能调用对象的方法。
在我们思考问题的时候,常常会犯一些习惯性错误。又一次,我找一个朋友借车。我拿着车钥匙就去车库了
。到了车库我才发现那里停有许多的车,于是不得不打在电话询问。因为,我急于开车而忘了问是什么车。
同样,许多朋友在讨论接口到对象的映射关系时,也往往忽略了对象本身的映射问题。
其实,接口指针所对应的方法表中并不是直接存储的对象方法地址,而存储的是实现每一个接口方法到对象
方法跳转的一小段代码的地址。这一小段代码首先将接口指针转换为对象指针,然后才跳转到对象的方法地
址,调用其值向对象的方法。
在DELPHI中,一个接口对象的实例空间中存有该对象所实现的所有接口的方法表指针。如果将对象实例空间
中的接口方法表指针看作一个对象的数据成员,则该数据的地址值就是一个接口引用值,即接口指针。实际
上,接口方法表指针就是一个接口对象的数据成员,它在对象实例空间中的偏移是固定的。因此,DELPHI只
需要将接口指针减去这个固定偏移就可得到对象的指针。接口方法到对象方法跳转的那小段代码,就是实现
了这种映射。
总之,接口就是虚方法地址表的说法并不完全正确,接口的实现有更多要考虑的东西。但这并不影响我们使
用DELPHI开发复杂的应用程序。这只是DELPHI内部实现接口机制的奥秘,应该由缔造DELPHI的大师们去考虑
。我们还是来关心一下程序语言一级的接口方法映射问题吧。
在默认的情况下,我们都将接口类的方法与所实现接口类型的方法取成相同的名称,就好像重载类的虚函数
一样。但接口类的方法与接口类型的方法之间不是重载关系,而是对应关系。没有任何人说过接口接口类的
方法的名称一定要与接口类型的方法相同,只是这种相同的名称可以让DELPHI编译器自动地对应接口和类的
方法。
假设我们定义了一个IMailBox的接口如下:
IMailBox = interface
procedure PutMail( aMail : string);
function GetMail : string;
end;
我们用下面的TMailBox类来实现这一接口:
TMailBox = class(TInterfacedObject, IMailBox)
procedure PutMail( aMail : string);
function GetMail : string;
end;
由于这个TMailBox的方法名称与IMailBox的方法名称完全相同,所以DELPHI编译器会自动将接口的方法映射
到相关的类对象的方法上。
但如果我们改用下面的TMailBox类实现这一接口:
TMailBox = class(TInterfacedObject, IMailBox)
procedure SetMail( aMail : string);
function GetMail : string;
end;
这时,编译器无法在TMailBox的定义中找到一个与IMailBox的PutMail方法相匹配的定义,将提示
“Undeclared identifier: 'PutMail'”。其实,我们的意思是要用TMailBox的SetMail方法实现IMailBox接
口的PutMail方法,只是它们的名称不同。但编译器不知道,它还没有聪明到理解自然语言的地步。
这时候,我们需要用到接口方法映射语句。重新定义TMailBox如下:
TMailBox = class(TInterfacedObject, IMailBox)
procedure IMailBox.PutMail = SetMail; //接口方法映射定义
procedure SetMail( aMail : string);
function GetMail : string;
end;
其中的procedure IMailBox.PutMail = SetMail;一句,是告诉编译器:IMailBox的PutMail要映射到类的
SetMail方法上。这样,程序就可以正常编译通过了。
这再次说明,对象类与接口不是继承关系,对象类方法与接口方法也不是虚函数的重载关系。而是对象类实
现了接口,接口方法被映射到对象方法。
你可以将定义对象类想象为设计一块电路板。如果,你当初就是按照某总接口标准来设计电路板的输入和输
出引脚,那么电路板设计好后肯定可以直接连接到标准接口上。否则,就要再焊接一些跳线,以实现接口引
脚到电路板引脚的映射。
接口概念的好处就是,使用接口的人也许永远不知道接口功能是怎样实现的,但这种功能却应用得很好。而
接口的使用者就可采用尽可能灵活的方式实现接口功能,不用担心外人会窃取你的实现技术机密。
在DELPHI中一个对象类实现接口时,它可以将这种实现要求委托给另外一个对象或接口来完成。这使得接口
功能的提供者有了一种更灵活的接口实现方法。
我们来看看下面的程序:
program ServiceCenter;
type
IWasher = interface
procedure WashClothing;
end;
IRemover = interface
procedure MoveHouse;
end;
TWasher = class(TInterfacedObject, IWasher)
procedure WashClothing;
end;
TRemover = class(TInterfacedObject, IRemover)
procedure MoveHouse;
end;
TServiceCenter = class(TInterfacedObject, IWasher, IRemover)
private
FWasher : TWasher; //洗衣工对象
function FindRemover : IRemover; //找寻搬运工
public
constructor Create;
property Washer:TWasher read FWasher implements IWasher; //委托实现
property Remover:IRemover read FindRemover implements IRemover; //委托实现
end;
procedure TWasher.WashClothing;
begin
end;
procedure TRemover.MoveHouse;
begin
end;
constructor TServiceCenter.Create;
begin
inherited;
FWasher := TWasher.Create;
end;
function TServiceCenter.FindRemover:IRemover;
begin
result := TRemover.Create;
end;
begin
end.
在这个程序中我们分别定义了洗衣工和搬运工的接口IWasher和IRemover,以及它们的实现类TWasher和
TRemover。我们又定义了一个服务中心的类TServiceCenter,它实现IWasher和IRemover的接口。服务中心是
可以提供洗衣和搬家的服务的,但它并不自己去洗衣和搬家,而是委托给其他洗衣工和搬运工。其中的两条
定义语句:
property Washer:TWasher read FWasher implements IWasher; //委托实现
property Remover:IRemover read FindRemover implements IRemover; //委托实现
完成这种服务的委托关系。
服务中心对象可以被任何需要IWasher和IRemover的接口引用,但它只是一个中间商,具体的工作是由
TWasher和TRemover来完成的。
其中,TServiceCenter在创建的时候建立一个TWasher对象来实现IWasher接口,为客户提供洗衣服务。因为
洗衣的服务是经常性的服务,有必要在服务中心开张的时候就雇佣洗衣工人。但是在需要提供IRemover接口
的时候,它却动态建立一个TRemover对象并返回接口。因为搬家不是经常的,在需要的时候临时雇一个搬运
工来干活就行了。
总之,接口功能委托实现是很灵活的,而且可以是动态的。了解这些奥秘,你就能编写出更好的基于接口的
程序。不过程序应该贴近和反映生活中的实际事物,这才叫面向对象。
本节的最后卖两个关子留给大家思考:
1. 在TServiceCenter的Create构造函数中建立了FWasher对象,有必要在相应的析构函数Destroy中释放吗
?
2. 是否有必要对TServiceCenter临时建立的TRemover的对象进行内存管理呢?
相信你能正确分析和解答这两个问题。
接口
对于Object Pascal语言来说,最近一段时间最有意义的改进就是从Delphi3开始支持接口(interface),
接口定义了能够与一个对象进行交互操作的一组过程和函数。对一个接口进行定义包含两个方面的内容,一
方面是实现这个接口,另一方面是定义接口的客户。一个类能实现多个接口,即提供多个让客户用来控制对
象的“表现方式”。
正如名字所表现的,一个接口就是对象和客户通信的接口。这个概念像C++中的PUREVIRTUAL类。实现接
口的函数和过程是支持这个接口的类的工作。
在这里你将学到接口的语言元素,要想在应用程序中使用接口,请参考COM和ActiveX方面的资料;
1.定义接口
就像所有的Delphi类都派生于TObject一样,所有的接口都派生于一个被称为是IUnknown的接口,
IUnknown在system单元中定义如下:
IDispatch = interface(IUnknown)
['{00020400-0000-0000-C000-000000000046}']
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: Integer;
DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var
Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
end;
正如你所看到的,接口的定义就像是类的定义,最根本的不同是在接口中有一个全局唯一标识符(GUID)
,它对于每一个接口来说是不同的。对IUnknown的定义来自于Microsoft的组件对象模型(COM)规范。
如果你知道怎样创建Delphi的类,那么定义一个定制的接口是一件简单的事情,下面的代码定义了一个
新的接口称为IFoo,它包含一个被称为F1()的方法:
type
IFoo = Interface
['{2137BF60-AA33-11D0-A9BF-9A4537A42701}']
function F1 : Integer;
end;
提示在Delphi的IDE中,按Ctrl+Shift+G键可以为一个接口生成一个新的GUID。
下面的代码声明了一个称为IBar的接口,它是从IFoo接口继承来的:
type
IFoo = Interface(IFoo)
['{2137BF61-AA33-11D0-A9BF-9A4537A42701}']
function F2 : Integer;
end;
2.实现接口
下面的代码演示了在一个类TFooBar中怎样实现IFoo和IBar接口:
type
TFooBar = class(TInterfacedObject, IFoo, IBar)
function F1 : Integer;
function F2 : Integer;
end;
function TFooBar.F1 : Interger;
begin
Result := 0;
end;
function TFooBar.F2 : Interger;
begin
Result := 0;
end;
注意,一个类可以实现多个接口,只要在声明这个类时依次列出要实现的接口。编译器通过名称来把接
口中的方法与实现接口的类中的方法对应起来,如果一个类只是声明要实现某个接口,但并没有具体实现这
个接口的方法,编译将出错。
如果一个类要实现多个接口,而这些接口中包含同名的方法,必须把同名的方法另取一个别名,请看下
面的程序示例:
type
IFoo = Interface
['{2137BF60-AA33-11D0-A9BF-9A4537A42701}']
function F1 : Integer;
end;
type
IBar = Interface
['{2137BF61-AA33-11D0-A9BF-9A4537A42701}']
function F1 : Integer;
end;
TFooBar = class(TInterfacedObject, IFoo, IBar)
//为同名方法取别名
function IFoo.F1 = FooF1;
function IBar.F1 = BarF1;
//接口方法
function FooF1 : Integer;
function BarF1 : Integer;
end;
function TFooBar.FooF1 : Interger;
begin
Result := 0;
end;
function TFooBar.BarF1 : Interger;
begin
Result := 0;
end;
3.implements指示符
implements指示符是在Delphi4中引进的,它的作用是委托另一个类或接口来实现接口的某个方法,这个
技术有时又被称为委托实现,关于implements指示符的用法,请看下面的代码:
type
TSomeClass = class(TInterfacedObject, IFoo)
//Stuff
function GetFoo : TFoo;
property Foo : TFoo read GetFoo implements GetFoo;
//Stuff
end;
在上面例子中的implements指示符是要求编译器在Foo属性中寻找实现IFoo接口方法。属性的类型必须是
一个类,它包含IFoo方法或类型是IFoo的接口或IFoo派生接口。implements指示符后面可以列出几个接口,
彼此用逗号隔开。
implements指示符在开发中提供了两个好处:首先,它允许以无冲突的方式进行接口聚合。聚合
(Aggregation)是COM中的概念。它的作用是把多个类合在一起共同完成一个任务。其次,它能够延后占用实
现接口所需的资源,直到确实需要资源。例如,假设实现一个接口需要分配一个1MB的位图,但这个接口很少
用到。因此,可能平时你不想实现这个接口,因为它太耗费资源了,用implements指示符后,可以只在属性
被访问时才创建一个类来实现接口。
4.使用接口
当在应用程序中使用接口类型的变量时,要用到一些重要的语法规则。最需要记住的是,一个接口是生
存期自管理类型的,这意味着,它通常被初始化为nil,它是引用计数的,当获得一个接口时自动增加一个引
用计数;当它离开作用域或赋值为nil时它被自动释放。下面的代码演示了一个接口变量的生存期自管理机制
。
var
I : ISomeInterface;
begin
//I被初始化为nil
I := FunctionReturningAnInterface; //I的引用计数加1
I.SomeFunc;
//I的引用计数减1,如果为0,则自动释放。
end;
关于接口变量的另一个规则是,一个接口变量与实现这个接口的类是赋值相容的,例如,下面的代码是
合法的:
procedure Test(FB : TFooBar)
var
F : IFoo;
begin
F :=FB; //合法,因为FB支持IFoo
.
.
.
最后,类型强制转换运算符as可以把一个接口类型的变量强制类型转换为另一种接口。示例如下:
var
FB : TFooBar;
F : IFoo;
B : IBar;
begin
FB := TFooBar.create;
F :=FB; //合法,因为FB支持IFoo
B := F as IBar; //把F转换为IBar