delphi中的引用计数接口

delphi中的引用计数接口

delphi中的引用计数接口(Interfaces)
摘要:本文适用于非托管的windows和linux程序中的接口(Interfaces),着重点在于开发人员在使用Interfaces时遇到的实际问题。

为什么使用Interfaces
Interfaces使我们可以在独立的实现模块编写代码,这对我们编写相对复杂的应用程序是非常有用的,更重要的体现是在我们决定分离程序到包的时候。合理的利用Interfaces,我们可以在包里修改实现类,重新编译包而仍然使用原有的应用程序。Interfaces也允许我们编写松耦合的类结构从而实现
更加灵活、可维护的程序。

Interfaces的历史

Delphi3是第一个支持Interfaces的delphi版本,不过早在delphi2就有一种方法使用和开发COM接口,这是怎么实现的呢?答案很简单,如果你忽略一个类可以实现多个Interfaces的实事,那么你可以把Interfaces理解成一个纯粹的抽象类。

type
  IIntf1 = class
  public
    function Test; virtual; abstract;
  end;

  IIntf2 = interface
  public
    function Test;
  end;
很明显,IIntf1有着很多局限性,但这就是delphi2中实现COM接口的方式。这两种结构的相似之处在于虚拟方法表(VMT)的结构上,你可以把一个Interface看作是一个VMT的定义.delphi3宣称支持原生的Interface,废弃了IIntf1这种结构,同时也增加了Object Pascal中最大的改进:多个接口的实现。
type
  IIntf1 = interface ... end;
  IIntf2 = interface ... end;
  TImplementation = class(TAncestor, IIntf1, IIntf2) ... end;  // 1
  如果IIntf1和IIntf2被声明成抽象类,那么在行//1中的语句将会报错。
 
  Interface 的实现
  现在我们已经决定在我们的程序中使用Interface,那么我们就必须解决在程序声明和实现部分的几个问题。
   
  GUIDs
 
 Interface和抽象类最大的不同就是:Interface必须有一个GUID。 GUID是一个128位的常量,在delphi中被用做 Interface唯一的标志。你可能在COM中就已经见过GUID,delphi使用与COM中相同的机制来获得Interface的使 用权。
 type
  ISimpleInterface = interface
    ['{BCDDF1B6-73CC-406C-912F-7148095F1F4C}'] // 1
  end;
  GUID出现在行//1处,就像你所看到的,在行//1处的GUID并不是一个128位的整数,而是一个字符串,delphi的编译器可以识别这种格式的字符串,并把它转换成GUID结构。
type
  TGUID = packed record
    D1: LongWord;
    D2: Word;
    D3: Word;
    D4: array[0..7] of Byte;
  end;
 
在定义一个GUID常量时,这种用于表示128位整数的字符串格式同样适用。
type
  IID_ISimpleInterface: TGUID = '{BCDDF1B6-73CC-406C-912F-7148095F1F4C}';
 
 为什么GUIDs很重要
 为什么一个Interface需要唯一的标志符呢?答案很简单,因为delphi类可以实现多个接口,当一个程序运行的时候,必须有一种机制能够得到从实现部分指向恰当接口的指针。确认一个对象是否实现一个接口并取得指向该接口实现部分的一个指针,唯一方法就是通过GUIDs。
 
 Interface的核心方法
 function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
让我们从简单的一个方向入手:_AddRef 和 _Release,就像你从它们的名字大概猜出的一样,_AddRef增加一个引用计数而_Release则减少一个。_Release 的行为依赖在实现过程中引用的class,interface管理部分最关键的方法是QueryInterface,它掌握着一个interface的GUID以取得和返回指向它的实现部分的一个指针。对于COM的相同部分来说,这个方法返回OLE HResult 类型的值。

QueryInterface,操作者和委托者

QueryInterface怎么被称作操作者和委托者呢?答案是简单的,QueryInterface被用来获取从实现类到Interface的指针。让我们考虑一下这个代码片断。
type
  TCls = class(TInterfacedObject, IIntf1, IIntf2)
  protected
    // implementation of interfaces.
  end;

var
  C: TCls;
  I1: Intf1;
  I2: Intf2;
begin
  C := TCls.Create;
  I1 := C; // 1
  I2 := C; // 2

  // call methods of I1 and I2

  I1 := nil;
  I2 := nil;
end;
在行//1和//2处的代码被作为_IntfCast过程进行了编译。这个过程被称为QueryInterface,返回一个在实现实例中指向对应interface的指针,它也释放了目标变量的初始值。
procedure _IntfCast(var Dest: IInterface; const Source: IInterface; const IID: TGUID);
var
  Temp: IInterface;
begin
  if Source = nil then
    Dest := nil
  else
  begin
    Temp := nil;
    if Source.QueryInterface(IID, Temp) <> 0 then
      Error(reIntfCastError)
    else
      Dest := Temp;
  end;
end;

如果我们要进行如下构造,那么这些代码的正确性必须地得到保证。
I1 := Impl as ISimpleInterface;

如果QueryInterface返回一个“nil”值,那么这里的"as"和":="操作符将会引发一个EIntfCastError错误,如果你希望避免异常操作,那么用QueryInterface来代替:
Impl.QueryInterface(IAnotherInterface, A);

QueryInterface是delphi的Interface中的核心方法之一,其他两个方法_AddRef 和 _Release则通常被用在一个interface中进行声明周期的控制。

Interface 的创建和销毁

Interface由被成为实现部分的构造器进行创建,然后RTL复制一个接口指针从已经创建的实现实例指向Interface变量。你可能已经猜到了接口的复制首先是一个简单的指针分配,然后是增加引用计数。为了增加引用计数,RTL调用由实现部分基类提供的_AddRef方法,让我们来看一下在行//1和//3处的delphi伪码:
// line1
begin
  var C: TSimpleImplementation := TSimpleImplementacion.Create;
  if (C = nil) then Exit;
  var CVMT := C - VMTOffset;
  _IntfCopy(Intf, CVMT);
end;

line1的代码构造了一个实现类的实例,获取指向它的VMT的指针并调用函数_IntfCopy,最重要的代码片断就是_IntfCopy:

procedure _IntfCopy(var Dest: IInterface; const Source: IInterface);
var
  OldDest: Pointer;
begin
  OldDest := Dest;                     // 1
  if Source <> nil then
    Source._AddRef;                    // 2
  Dest := Source;
  if OldDest <> nil then
    IInterface(OldDest)._Release;      // 3
end;
在大部分情况下,interface委托意味着分配非空的指针指向已存在的接口。如果一个目标接口是非空的(即它已经引用了一个切实存在的接口),那么它必须在被成功分配到新接口以后进行释放。这就是为什么在行//1处的代码复制原有的目标到一个临时的变量中去。然后该过程为源接口增建引用计数,在实际的委托操作之前增加引用计数是很重要的。如果该过程没有做这些,另外一个线程可能会释放该接口在_IntfCopy结束它之前。这将导致分配一个空的实例,从而引起一个存取违例错误。因此,行//2处在复制源接口的值到目标接口之前,为源接口增加了一个引用计数。最后,这个源接口被分配到另外一个接口,这个接口调用了_Released。一旦接口被创建,引用计数增加并且目标接口被指派到新创建的接口,我们就可以安全地调用它的方法。

// line 2:
begin
  var ImplVMT = Intf + VMTOffset;
  (ImplVMT + MethodOffset)(); // 2 
end;
Bearing in mind that an interface is simply a VMT template a method call must be a call to a method that is looked up in implementation’s VMT. In our simple example, Test is the only virtual method if the implementation, MethodOffset is going to be 0, and VMTOffset is going to be $0c. The actual compiled code looks like this:
记住,一个接口仅仅是一个VMT中的一个方法调用,必须是一个调用一个在实现部分的VMT中能够找到的方法。在我们简单的例子中,Test是实现部分的唯一虚拟方法,MethodOffset将会置0,而VMTOffset会置$0c,实际的编译代码就像这样:
// set eax to the address of the first local variable
mov    eax, [ebp - $04]
// edx := @eax
mov    edx, [eax]
// call to ((procedure of object)(edx + VMTOffset + MethodOffset))()
call   dword ptr [edx + $0c]

这段代码事实上调用了实现类中的Test方法,和调用常规的虚方法没有太多不同。原来列表中的第三行和第一行一样重要,因为它负责销毁这个接口。很重要的一点,一个接口的引用计数达到0时,该实现类被销毁。危险在于这个指向实现部分的指针可能仍然没有变化,因此设置了一个针对实现部分是否为空的测试,用以检测一个不能确定是否依然存在的实现部分。
// line 3:
begin
  _IntfClear(Intf);
end;
As you can tell, the most important code is hidden in _IntfClear method. This method must _Release the interface, and (if appropriate, free the implementation).
像你能说的一样,最重要的代码被隐藏在_IntfClear方法里,该方法必须_Release这个接口,并(如果合适,释放这个实现部分)。

function _IntfClear(var Dest: IInterface): Pointer;
var
  P: Pointer;
begin
  Result := @Dest;
  if Dest <> nil then
  begin
    P := Pointer(Dest);
    Pointer(Dest) := nil;                      // 1
    IInterface(P)._Release;              // 2
  end;
end;

行//1设置目标接口为nil,而行//2释放这个接口。当引用计数为0的时候,_Release方法必须调用实现部分的析构器。让我们来看一下我们测试单元中的编译代码。

// load effective address of the first local variable
lea    eax, [ebp - $04]
// in _IntfClear:
// edx := @eax
mov    edx, [eax]
// if (edx = nil) then goto $0e (end);
test   edx, edx
jz     $0e
// eax^ := 0;
mov    [eax], 0
// push original value of eax
push   eax
// push Self parameter
push   edx
// eax := @edx
mov    eax, [edx]
// call _Release.
call   dword ptr [eax + $08]
// restore eax
pop    eax

最重要的事情是了解原始列表中行//3后面的内容,接口为nil并且实现部分被销毁。这里面的危险可能比代码片断里更显而易见:
var
  Impl: TSimpleImplementation;
  Intf: ISimpleInterface;
begin
  Impl := TSimpleImplementation.Create;
  Intf := Impl;
  Intf.Test;
  Intf := nil;
  if (Impl <> nil) then Impl.Free; // 1
end;

行//1处的危险:在接口的引用计数变成0后,实现部分的析构器被调用了;然而,指向实现部分实例的指针的值仍然保持为非空。行//1将导致调用析构器对已经销毁的实例进行销毁,在大部分情况下,将引发一个存取异常。

实现部分自动销毁的含义

销毁机制的含义是什么?也许最重要的一点就是:如果你希望保持你的代码可以轻松维护,那么你应该从不设置实现部分和接口的变量。另外一个问题是你必须做一些额外的编码,如果你想动态地使用你的实现部分。让我们考虑一下这个状况:一个类方法返回一个接口,可是你不希望每次调用这个方法时都实例化一个实现类。
type
  TCls = class
  public
    function GetInterface: ISimpleInterface;
  end;

忘记这些析构规则是非常容易的,尤其是写如下代码时:
type
  TCls = class
  private
    FImpl: TSimpleImplementation;
  public
    constructor Create;
    destructor Destroy; override;
    function GetInterface: ISimpleInterface;
  end;

constructor TCls.Create;
begin
  inherited Create;
  FImpl := TSimpleImplementation.Create;
end;

destructor TCls.Destroy;
begin
  if (FImpl <> nil) then FImp.Free;
  inherited;
end;

function TCls.GetInterface: ISimpleInterface;
begin
  Result := FImpl;
end;
第一个措施是用实现部分的实例代替了接口。你将遇到的问题(存取违例,甚至更明确的)是混淆了实现部分的销毁过程。
当这个类正确运行时,如果方法GetInterface没有被调用,那么该实例不会出错。相反,一旦GetInterface被调用,一个错误就会在TCls的析构器中产生,如果它被调用超过一次,在你打算调用ISimpleInterface’s的test方法时一个错误将会发生。避免这种混乱的方法是使用正确的基础实现类:delphi的system单元提供了三个基本的实现类:TInterfacedObject, TAggregatedObject和TContainedObject. 这三个类提供线程安全的接口实现。

TInterfacedObject

这是一个最简单的接口实现类,关于线程安全的实现部分,有相当有趣的含义。首先,TInterfacedObject必须确认接口在被完全构造之前没有被释放。这种状况在多线程的程序中很容易发生。考虑这样一种情况,线程构造了一个接口实现类的实例用以存取接口。在该实例完全构造完成以前,线程2释放了先前已经获得的该类型接口,这将出发释放机制。因此如果这种状况没有被考虑到,将导致的过早释放这个接口。接下来的代码直接取自delphi的system单元:
procedure TInterfacedObject.AfterConstruction;
begin
// Release the constructor's implicit refcount. Thread-safe increase is
// achieved using Win API call to InterlockedDecrement in place of Dec
  InterlockedDecrement(FRefCount);
end;

procedure TInterfacedObject.BeforeDestruction;
begin
  if RefCount <> 0 then
    Error(reInvalidPtr);
end;

// Set an implicit refcount so that refcounting
// during construction won't destroy the object.
class function TInterfacedObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TInterfacedObject(Result).FRefCount := 1;
end;

function TInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TInterfacedObject._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);
end;

function TInterfacedObject._Release: Integer;
begin
// _Release thread-safely decreases the reference count, and
  Result := InterlockedDecrement(FRefCount);
// if the reference count is 0, frees itself.
  if Result = 0 then
    Destroy;
end;

这是在接口支持总最重要的代码,它对理解接口实现部分的创建和销毁很重要。现在让我们将注意力转移到另外的基础实现类。

TContainedObject and TAggregatedObject

这两个类应当被用在有关于接口属性内容时。它们都保持一个到接口实现的微弱的引用。

type
  TCls2 = class(T[Contained|Aggregated]Object, ISimpleInterface)
  private
    function GetSimple: ISimpleInterface;
  public
    property Simple: ISimpleInterface read GetSimple
      implements ISimpleInterface;
  end;

function TCls2.GetSimple: ISimpleInterface;
begin
  Result := Controller as ISimpleInterface;
end;

var
  C: TCls2;
begin
  C := TCls2.Create(TSimpleImplementation.Create); // 1
  // Call interface methods
  C.Free;                                          // 2
end;

行//1和行//2展示了TInterfacedObject和TContainedObject的不同。首先,是实现子句,你不是必须在TCls2中实现ISimpleInterface的方法,相应的,TCls2必须提供有个属性和一个选择方法以取得有个指向ISimpleInterface的指针。这个选择方法的实现部分为Simple从控制器取得了接口。一个控制器的实例作为构造方法的参数被传递。也许TContainedObject和TInterfacedObject的最大不同是销毁机制。你必须手工释放一个TContainedObject实例,因为没有自动销毁机制可调用。然而,这个容器类的自动销毁调用依然存在。

对于那些有意被聚合或者包含在一个外部的控制对象中的借口对象,TAggregatedObject 和 TContainedObject 是合适的基类。当我们在一个外部对象类的声明中,将“implements”用于接口属性上时,使用这些类型来实现内部对象。由依靠控制器的优势的聚合对象实现的借口应当与其它控制器提供的接口有所区别。聚合对象一定不能保持它们自己的引用计数,因为它们必须和他们的控制器有相同的生命周期。为了实现这个目标,聚合对象映射引用计数方法到控制器。TAggregatedObject简单地映射QueryInterface方法到它的控制器。这样一个聚合对象,能够获取其它由其控制器支持的接口,注意仅仅是被支持的接口。这对于实现一个控制器类(即用一个或多个内部对象来实现其声明的借口的控制器类)来说,是非常有用的。聚合允许实现部分越过对象的层次进行共享。大部分聚合对象应该从TAggregatedObject继承,特别是当与“implements”语法联合使用时。
让TCls2 成为TAggregatedObject的派生类:在这种情况下,我们可以这样写代码:
var
  C: TCls2;
begin
  C := TCls2.Create(TSimpleImplementation.Create);
  C.Simple.Test;                          // 1
  (C as ISimpleInterface).Test;           // 2
  C.Free;
end;

行//1处是合法的;它仅仅是取得一个指针,指向利用GetSimple选择方法的ISimpleInterface,这个选择方法则用于从控制器取得一个适当的接口。行//2是非法的,因为TAggregatedObject只能用于控制器以返回一个适当的接口。

TContainedObject的用途是从控制器隔离聚合体上的QueryInterface方法。从这个类继承的派生类将只能返回这个类自身实现的接口,而不是控制器。这个类被用于实现那些与控制器拥有相同生命周期的接口。这个设计模式被称为forced encapsulation,让TCls2成为TContainedObject的派生类:
var
  C: TCls2;
begin
  C := TCls2.Create(TSimpleImplementation.Create);
  C.Simple.Test;                          // 1
  (C as ISimpleInterface).Test;           // 2
  C.Free;

不像前一个例子,我们可以使用
C.Simple.Test和(C as ISimpleInterface).Test两种方式。

总结

接口是非常强大的工具,用于编写灵活可扩展的应用程序。然而正像每一种强大的工具一样,如果你不清楚你想做什么和编译器准备如何编译这些代码,它们可能是非常危险的。在下一篇文章中,我将集中讲述.NET接口和delphi.NET编译问题 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值