[转帖]接口的真相
接口的真相
接口(interface)在Delphi中是一个很有意思的东西。Delphi 3开始支持接口,从而形成了COM编程的基础;然而,Delphi中的接口也可用在非COM开发中,实现类似抽象类(含有抽象方法的类)的功能,从而弥补了Delphi中不能多继承(子类有多个同级父类)的不足。这里所讲的interface和一个单元中的interface部分是完全不同的概念,不要混淆。
说了半天,似乎还没有解决接口是什么的问题。接口就是一组功能的实现者和使用者之间的协议。当我看到了实现一组功能的必要性,但是其实现方式又是不能确定的且可以有很多途径,这时候就要定义接口。接口不关心功能的具体实现过程,但是规定了功能需要的输入条件和输出结果。
也就是说,接口规定了功能的定义,但是必须由类具体实现这些功能。所以,接口的概念类似于C++的纯虚类(Pure Virtual Class)。
本书专列一小节来讲述接口,是为了使大家理解Delphi中接口的真正含义。在本书的开发实例中,基本不涉及接口的应用。
举个例子:
type
IShowString = interface(IUnknown)
procedure ShowString(S: String);
end;
TIObject = class(TObject, IShowString)
procedure ShowString(S: String);
end;
上面代码中首先定义一个接口IShowString,它声明了一个方法ShowString。类TIObject从TObject继承,同时实现了接口IShowString。
一个类可以同时实现一个或者多个接口,如:
TIObject = class(TObject, I1, I2, I3);
接口的等级关系和类是相似的。IUnknown是接口的祖先,就像类的TObject一样。如下的声明:
type
TOneObject = class
end;
TOneInterface = interface
end;
表示TOneObject从TObject派生,TOneInterface从IUnknown派生。接口也不能同时有多个同级父接口。
在以下部分,我希望从不同角度来展示接口的具体内容。
1. 接口和类的不同
(1)接口中只有方法、属性,没有字段。所以接口中的属性存取都必须通过方法。
(2)接口中的方法只有声明,没有实现。实现在类中完成。
(3)接口中的方法没有作用域。都是公开的,但是在类中则放到不同作用域。
(4)接口中的方法没有修饰字。可以认为它们都是抽象的。
(5)不能创建接口实例,要使用接口,必须在类中实现,通过类调用接口的方法。
(6)在类中必须声明和实现接口的所有方法。不像类继承中可以选择。
(7)接口中的方法在类中实现时,可以加virtual/dynamic、abstract修饰,从而在子类中可以实现覆盖。如:
type
IShowString = interface(IUnknown)
procedure ShowString(S: String);
end;
TComponent1 = class(TComponent, IShowString)
protected
procedure ShowString(S: String); virtual;
end;
TComponent2 = class(TComponent1)
protected
procedure ShowString(S: String); override;
end;
2. 接口标示
声明接口的典型语法是:
ChildInterface = interface(ParentInterface)
['{GUID}']
{方法列表}
end;
其中的['{GUID}'](Globally Unique Identifier,全球惟一标示)称为接口标示。COM类等可以有GUID标示。这样我们可以通过GUID得到对应的接口或者COM类(实例)。接口标示不是必须的。在IDE环境中,按Ctrl+Shift+G键可以产生GUID,也可以调用API函数CoCreateGuid得到。如果父接口定义了标示而它的子接口没有定义,该标示不会继承到子接口,此时子接口被认为没有标示。Delphi的SysUtils单元提供了GUID和String之间的转换函数StringToGUID、GUIDToString。
3. 祖先IUnknown(System单元)
IUnknown是这样声明的:
IUnknown = IInterface;
IInterface = 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点,我们知道,任何类要想实现接口,就必须实现上面三个方法(当然同时实现多个接口时,三个方法只需要一次实现)。这是不是很麻烦?幸运的是,Delphi内部自动实现了这三个方法,所以:
(1) 如果你的类从TObject、TPersistent派生,请分别使用TInterfacedObject和TInterfacedPersistent代替TObject、TPersistent,它们内部实现了这三个方法。如:
TIObject = class(TInterfacedObject, IShowString)
(2)TComponent则直接实现了这三个方法:
TComponent = class(TPersistent, IInterface,
IInterfaceComponentReference)
protected
{ IInterface }
function QueryInterface(const IID: TGUID; out Obj): HResult;
virtual; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
所以,从TComponent及其子类派生的类可以实现任何接口而不需要考虑这三个方法的实现。
4. 接口方法的调用
(1)直接分配。如:
var
IShowStr: IShowString;
begin
{类和它们实现的接口是兼容的}
IShowStr := TComponent2.Create(nil);
IShowStr.ShowString('dd');
{接口引用计数方法会最终销毁接口所属的对象,所以不需要显式销毁对象。我们下面会详细
讲述这个问题。}
end;
(2)使用TObject.GetInterface方法。使用这个方法时,接口必须指定了标示。定义如下:
function GetInterface(const IID: TGUID; out Obj): Boolean;
调用如:
var
IShowStr: IShowString;
begin
TComponent2.Create(nil).GetInterface(IShowString, IShowStr);
IShowStr.ShowString('dd');
end;
(3)使用RTTI的as操作符。此时接口也必须指定了标示。如:
begin
(TComponent2.Create(nil) as IShowString).ShowString('dd');
end;
(4)如果类将接口方法声明在公开域,可以直接用类实例调用接口方法,这时候接口方和类本身的方法没有任何区别。
可以用下面的方法判断一个接口、对象、类是否支持某个接口:
function Supports(const Instance: IInterface; const IID: TGUID;
out Intf): Boolean; overload;
function Supports(const Instance: TObject; const IID: TGUID;
out ntf): Boolean; overload;
function Supports(const AClass: TClass; const IID: TGUID): Boolean;
overload;
5. 接口引用计数
接口引用计数是由_AddRef和_Release实现的。实现这两个方法的类中会有一个整数字段(如TInterfacedObject.RefCount)。当引用一个接口时,_AddRef将RefCount加1,引用完毕后_Release将RefCount减1。如果RefCount减少到0时,_Release调用接口所属对象的Destroy方法销毁对象。大家看个例子:
var
Obj: TComponent2;
IShowStr: IShowString;
begin
Obj := TComponent2.Create(nil);
IShowStr := Obj;
Obj.Free;
IShowStr.ShowString('dd');
end;
当最后一句执行完后,会触发异常。因为IShowStr的方法调用完毕后,_Release将RefCount减到了0,于是调用Obj.Destroy;但是这时候Obj已经被销毁,因而异常。
接口引用可以自动计数,不需要显式地销毁它;但在一些实时性很强的程序中,你也可以使用类似如下格式来显式销毁接口实例:
IShowStr := nil;
但并不是说我们创建的对象就可以不显式地调用Free、FreeAndNil等销毁了。引用计数实现的内部其实是很复杂的,我们应该显式地销毁动态创建的对象。
6. 方法分辨
如果一个类实现了多个接口,而这些接口中有同名方法,那么应该如何区分这些接口?
(1)如果符合重载的原则(方法名相同,但是参数不同或者一个是过程、另一个是函数),可以用overload关键字声明成重载方法。
(2)如果完全相同。就需要使用方法分辨子句。例如:
type
IShowString1 = interface(IUnknown)
procedure ShowString(S: String);
end;
IShowString2 = interface(IUnknown)
procedure ShowString(S: String);
end;
TComponent1 = class(TComponent, IShowString1, IShowString2)
protected
{以下两行就是方法分辨子句}
procedure IShowString1.ShowString = ShowString1;
procedure IShowString2.ShowString = ShowString2;
procedure ShowString1(S: String);
procedure ShowString2(S: String);
end;
7. 接口授权
假设TComponent1实现了接口IShowString,现在TComponent2也需要实现IShowString,而且实现的功能和TComponent1完全一样,那么可不可以通过很简单的办法从TComponent1引用这个功能,而不需要重新抄写代码?
Delphi中提供了属性关键字implements来实现这个引用功能,称为代理或者授权。大概意思是这样的:
type
IShowString = interface(IUnknown)
procedure ShowString(S: String);
end;
TComponent1 = class(TComponent, IShowString)
procedure ShowString(S: String);
end;
TComponent2 = class(TComponent, IShowString)
IShowStr: IShowString;
{以下这句可以代替声明ShowString}
property ShowStr: IShowString read IShowStr implements IShowString;
end;
然后可以通过属性ShowStr引用接口方法。不过引用前必须“实例化” IShowStr。例如:
constructor TComponent2.Create(AOwner: TComponent);
begin
inherited;
IShowStr := TComponent1.Create(AOwner);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Component2: TComponent2;
begin
Component2 := TComponent2.Create(nil);
Component2.ShowStr.ShowString('dd');
end;
小结
本小节讲述了Delphi中接口的概念、作用、实现和使用方法。引入接口的目的有两个:
(1)模拟类的多继承关系;
(2)开发COM程序。
在VCL类库中,有一些基础类(如TComponent)和Web类(如TMultiModuleWebAppServices)等使用了接口来帮助实现一些功能。在COM类中则大量使用,如TInterfacedObject、TContainedObject等。
一般在开发非COM程序时,因为较少须要使用多继承功能,相应地也较少使用接口,特别是从已有的VCL类和组件扩展用户自定义类和组件时。
DELPHI虽然很好,但目前不流行,所以相关的资料相对比较少,以前我也是花了不少时间才有所得。
为了方便同行,有时间我会把一些经验在这里和大家分享。
用过的都知道,在DELPHI WIN32下使用接口会有点复杂。一般通过继承TInterfacedObject来实现接口,而TInterfacedObject子类实例是由DELPHI根据引用计数来自动管理释放的。稍不注意,很容易出现异常。
所以使用DELPHI的接口要注意以下事项:
1.当使用接口变量时,如:p:Interface,要注意p的生命期。
在方法内部定义的p,方法执行结束时,p的生命期才算结束。
定义全局的p,只有在应用退出时,p的生命期才算结束。
在类内定义p作为属性,只有该类实例释放时,p才算用完。
所以如果p引用的对象在p的生命期结束前就释放了,很容易出现一些莫名其妙的异常。
当然,你在用完p后能 p:=nil,是一个好的习惯。
2.对于表达式: (对象 as 接口).方法(..) ,可以认为DELPHI会产生一个局部的接口变量,所以如果对象在这局部范围退出前会被释放的话,退出局部范围时会触发异常。解决的办法是:p := 对象 as 接口,显式使用接口变量,用完就设为NIL。
3.尽量把接口作为参数。譬如:定义一过程 procedure aaa(const p:Interface) ;,如果使用时是:aaa(TTestClass.Create(..)),那么肯定会导致内存泄漏。
4.TInterfacedObject及其子类的实例,在创建后若未赋值给任何接口变量,需要手动释放。
这里介绍两个类,或许对你有用。
1.TCommonInterfaced。继承它可以实现接口,特点是屏蔽DELPHI的自动管理,由开发人员来手动管理实例的释放。
2.TEventInterfaced。继承它可以实现接口,特点是在激活实例的自动释放功能后,实例会在外部用完时由DELPHI来释放;若没有激活自动释放功能,则手动释放。这个类主要用在事件模型下,类似于JAVA的事件模型,监听器是以接口形式挂在事件源上的,只有当事件源撤销后,监听器才能释放。所以,如果监听器是继承自 TEventInterfaced,那么事件源在撤销前激活监听器的自动释放功能,那么DELPHI就能选择适当的时机释放监听器了。
源代码如下:
********************************************************************
unit euBase;
interface
uses Classes,SysUtils ;
Type
//普通的接口对象。当接口不再使用时不会自动释放对象。
TCommonInterfaced = class(TInterfacedObject)
protected
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
//专用在事件模型的接口对象。
//所以当需要释放实现接口的对象时,只要激活接口的自动释放功能,对象就会在不使用接口时自动释放
IEventInterface = interface(IInterface)
['{4A8C144E-07B9-4357-8BF0-EE57D2F69888}']
procedure ReleaseInIdle;
end;
{
专用在事件模型的接口对象。
当需要释放实现接口的对象时,只要激活接口的自动释放功能,
对象就会在不使用接口时自动释放。
}
TEventInterfaced = class(TInterfacedObject, IEventInterface)
private
mAutoRelease: Boolean;
procedure ReleaseInIdle;
protected
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
implementation
{ TCommonInterface }
{
****************************** TCommonInterfaced *******************************
}
procedure TCommonInterfaced.AfterConstruction;
begin
end;
procedure TCommonInterfaced.BeforeDestruction;
begin
end;
function TCommonInterfaced._AddRef: Integer;
begin
Result:=1 ;
end;
function TCommonInterfaced._Release: Integer;
begin
Result:=1 ;
end;
{ TEventInterface }
{
******************************* TEventInterfaced *******************************
}
procedure TEventInterfaced.AfterConstruction;
begin
inherited;
mAutoRelease:=false ;
end;
procedure TEventInterfaced.BeforeDestruction;
begin
inherited;
end;
procedure TEventInterfaced.ReleaseInIdle;
begin
mAutoRelease:=true ;
end;
function TEventInterfaced._AddRef: Integer;
begin
if (RefCount=0) and (mAutoRelease=false) then Inherited _AddRef() ;
Result:=Inherited _AddRef() ;
end;
function TEventInterfaced._Release: Integer;
begin
if (RefCount=2) and (mAutoRelease=true) then
begin
Inherited _Release() ;
Result:=Inherited _Release() ;
end
else if (RefCount<>1) or (mAutoRelease=true) then
Result:=Inherited _Release()
else
Result:=Inherited _Release() ;
end;
end.
*******************************************************************
USE的单元可能需要补充,因为源代码是摘录的。