Delphi 接口机制真相

Delphi 接口机制真相

分类: DELPHI2012-03-29 14:41 2161人阅读 评论(0) 收藏 举报

delphiinterfaceintegerstringclassfunction

接口(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的单元可能需要补充,因为源代码是摘录的。

时间: 2024-11-03 11:16:59

Delphi 接口机制真相的相关文章

delphi 接口Interface

学习 delphi 接口 一切都是纸老虎!!! 第四章          接口 前不久,有位搞软件的朋友给我出了个谜语.谜面是“相亲”,让我猜一软件术语.我大约想了一分钟,猜 出谜底是“面向对象”.我觉得挺有趣,灵机一动想了一个谜语回敬他.谜面是“吻”,也让他猜一软件术 语.一分钟之后,他风趣地说:“你在面向你美丽的对象时,当然忍不住要和她接口!”.我们同时哈哈大 笑起来.谈笑间,似乎我们与自己的程序之间的感情又深了一层.对我们来说,软件就是生活. 第一节  接口的概念 “接口”一词的含义太广泛

delphi 接口

第四章          接口 前不久,有位搞软件的朋友给我出了个谜语.谜面是“相亲”,让我猜一软件术语.我大约想了一分钟,猜 出谜底是“面向对象”.我觉得挺有趣,灵机一动想了一个谜语回敬他.谜面是“吻”,也让他猜一软件术 语.一分钟之后,他风趣地说:“你在面向你美丽的对象时,当然忍不住要和她接口!”.我们同时哈哈大 笑起来.谈笑间,似乎我们与自己的程序之间的感情又深了一层.对我们来说,软件就是生活. 第一节  接口的概念 “接口”一词的含义太广泛,容易引起误解.我们在这里所说的接口,不是讨论程

zw版_Halcon图像库delphi接口文件

Halcon图像库delphi接口文件,根据安装时用户设置的文件目录不同,会有所差异,笔者一般安装在delphi的import目录下.     参见:<zw版·全程图解Halcon控件安装(delphi版)>,http://www.cnblogs.com/ziwang/p/4850958.html      安装成功后,import目录下,会有一个文件:HALCONXLib_TLB.pas,大约3900k,     这个文件,就是Halcon图像库的delphi接口文件,纯delphi源码,7

Delphi接口

program Demo1; { Create Date: 2014-06-29 Author: P.S.M 1.接口Demo1 } {$APPTYPE CONSOLE} uses SysUtils; {定义接口} type ITestInterface = interface {GUID通过CTRL+G自动产生} ['{15EAD871-2B5E-4F51-A14E-7D518A2371EF}'] procedure Test; end; {TInterfacedObject 实现了_AddR

Delphi 的接口机制——接口操作的编译器实现过程(1)

学习COM编程技术也快有半个月了,这期间看了很多资料和别人的程序源码,也尝试了用delphi.C++.C#编写COM程序,个人感觉Delphi是最好上手的.C++的模版生成的代码太过复杂繁琐,大量使用编译宏替代函数代码,让初学者知其然而不知其所以然:C#封装过度,COM编程注定是要与操作系统频繁打交道的,需要调用大量API函数和使用大量系统预定义的常量与类型(结构体),这些在C#中都需手工声明,不够简便:Delphi就简单多了,通过模版创建的工程代码关系结构非常清晰,而且其能非常容易使用API函

Delphi 的接口机制——接口操作的编译器实现过程(2)

接口对象的内存空间 假设我们定义了如下两个接口 IIntfA 和 IIntfB,其中 ProcA 和 ProcB 将实现为静态方法,而 VirtA 和 VirtB 将以虚方法实现: [delphi] view plaincopyprint? IIntfA = interface procedure ProcA; procedure VirtA; end; IIntfB = interface procedure ProcB; procedure VirtB; end; 然后我们定义一个 TMyO

Delphi接口的底层实现(接口在内存中仍然有其布局,它依附在对象的内存空间中,有汇编解释)——接口的内存结构图,简单清楚,深刻 good

引言 接口是面向对象程序语言中一个很重要的元素,它被描述为一组服务的集合,对于客户端来说,我们关心的只是提供的服务,而不必关心服务是如何实现的:对于服务端的类来说,如果它想实现某种服务,实现与该服务相关的接口即可,它也不必与使用服务的客户端进行过多的交互.这种良好的设计方式已经受到很广泛的应用. 早在Delphi 3的时候就引入了接口的概念,当时完全是因为COM的出现而诞生的,但经过这么多版本的进化,Delphi的接口已经成为Object Pascal语言的一部分,我们完全可以用接口来完成我们的

Delphi 接口定义

Delphi Interface接口的定义 2011-04-20 14:54:11|  分类: Delphi|举报|字号 订阅 type  InterfaceName = interface(ancestorInterface)    ['{GUID}']    memberList  end;这里,ancestorInterface 和 GUID是可选的.在大多数方面,接口声明和类声明相似,但有以下限制:  1.memberList只能包含方法和属性,而不能包含数据成员(field).  2.

Delphi 接口(3)

type IGreetable = interface end; IMan = interface(IGreetable) function SayHello():string; end; TChinese = class(TinterfacedObject,IMan) procedure SetChinese(name: string); function SayHello():string; end; procedure MyProcedure() var AMan: IMan; Greet