Delphi的接口陷阱

2016-02-19 12:51 17 1 收藏

有一种朋友不在生活里,却在生命力;有一种陪伴不在身边,却在心间。图老师即在大家的生活中又在身边。这么贴心的服务你感受到了吗?话不多说下面就和大家分享Delphi的接口陷阱吧。

【 tulaoshi.com - 编程语言 】

Delphi的接口陷阱现在我所知的有两大陷阱:陷阱一、接口的类型转换陷阱a)       不能把一个对象引用强制转换成这个引用的类型没有声明实现的接口,即使这个对象实际实现了这个接口(呵呵,优点拗口)。b)       当把一个对象变量赋给一个接口变量,在把这个接口变量赋还给对象变量时,这个对象变量的地址已经变了,也就是不再是原来的对象了,而是指向一个错误的地址。例如:I1 = interface
      function Do: Boolean;
  end;
  TC1 = Class
      ATT1: Integer;
  end;
  TC2 = Class(TC1, I1)
      ATT2: Integer;
      function Do: Boolean;
  end;Intf1: I1;OBJ1: TC!;OBJ2: TC2;OBJ2 := TC2.Create;
  OBJ1 := OBJ2.
  I1(OBJ2).DO;正确。
  I1(OBJ1).DO;编译失败。因为OBJ1的类型TC1没有声明实现I1所以不能转换成I1,即使OBJ1确实实现了I1。还有,如果把对象转为接口再转回来也会有问题。OBJ2 := TC2.Create;OBJ2.ATT1 := 0;
  Intf1 := OBJ2;//正确。OBJ2 := Intf1;TC2(Intf1).ATT1 := 0; //运行期非法地址访问错误。OBJ2.ATT1 := 0; //运行期非法地址访问错误。也就是,从对象引用转换成指针引用后,地址改变了,但是由指针引用再转回对象引用时地址没有变回来(Delphi的bug?)。 陷阱二、接口的生存期管理依据我的常识(此处是编程常识,不是Delphi使用常识)来讲,我认为接口是不需要生存期管理的,因为接口根本不可能生成真正的对象。但是Delphi却又一次打击了我的常识(咦,为什么要说“又”呢?),它的接口是有生存期的,而且必须实现以下三个方法:    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;    function _AddRef: Integer; stdcall;function _Release: Integer; stdcall;每次都要实现这三个方法是比较麻烦的,而且更重要的是,我不知道Delphi什么时候用以及怎么用这三个方法?所以我也不知道怎么实现这三个方法。J如果不想自己实现这三个方法,你可以使用TComponent。因为TComponent已经实现了这三个方法,所以可以从它继承,就不用实现这三个方法了。这样就可以放心使用了吗?答案是否定的。因为Delphi在你把接口变量置为nil时偷偷的(因为很出乎我的意料)调用了_Release。function _IntfClear(var Dest: IInterface): Pointer;var  P: Pointer;begin  Result := @Dest;  if Dest nil then  begin    P := Pointer(Dest);    Pointer(Dest) := nil;    IInterface(P)._Release;  end;end;而_Release时又做了什么呢?function TComponent._Release: Integer;begin  if FVCLComObject = nil then    Result := -1   // -1 indicates no reference counting is taking place  else    Result := IVCLComObject(FVCLComObject)._Release;end;不是Com对象的话,就什么也没作。我们作的不是Com对象,是不是就没有任何问题了呢?答案依然是否定的,考虑如下情况:OBJ2 := TC2.Create;tryIntf1 := OBJ2;Intf1.DO;Finally    OBJ2.Free;    Intf1 := nil;End;会怎么样呢?会出非法地址访问错误。为什么?上面说过把接口引用设为nil时,会调用_IntfClear,而_IntfClear又会调用对象的_Release,而这时这个对象已经释放了,自然就出非法地址访问错误啦。有人说多此一举吗,接口引用只是个地址,没必要手动设为nil。OBJ2 := TC2.Create;tryIntf1 := OBJ2;Intf1.DO;Finally    OBJ2.Free;End;结果可能还会出你的意料,还是非法地址访问错误。为什么?因为Delphi编译器耍了个小聪明,它认为你忘记把这个地址引用置为nil了,所以你会自动给你加上,看来Delphi编译器聪明过头了J。怎么解决呢?方法1,先把接口引用置为nil,再释放对象。    Intf1 := nil;    OBJ2.Free;方法2,把接口引用强制转成指针类型再置为nil。    Pointer(Intf1) := nil;此时相当于直接把地址清零,不会调用_IntfClear。我倾向于使用第二种方法,这样你就不用考虑先释放谁的问题了。而且有些设计模式中你可能只持有接口引用,而且你也不知道引用的对象什么时候释放,此时就必须使用方法2。例如考虑Composite模式。TComposite = class(TComponent, I1)Private    interList: TXContainer;//一个容器类,存放“叶子”的接口引用。Public    Procedure Add (AIntf: I1);    function DO: Boolean;End;它应该释放它的“叶子”吗?显然不是,那“叶子”是不是一定会晚于这个“合成对象”对象释放呢?我想也不一定吧。如果强制这样规定的话,就失去很多的灵活性。所以我们肯定想这些接口引用置nil时,不会和原对象发生什么关系,以免对象被释放后出非法地址访问错误。考虑使用什么容器呢?array?TList?TInterfaceList?首先想到肯定是TInterfaceList了,因为我们是要容纳的就是接口。但是对他进行Free时,它会把它所有容纳的接口置为nil,这正是我们不想要的。或者我们可以在Free之前先把它存储的接口引用转为指针再置为nil。  for I := 0 to interList.Count -1 doPointer(interList.Items[i]) := nil;可惜的是,编译错误“[Error] XXXX.pas(XX): Left side cannot be assigned to”。然后我们试一下array。interList: Array of I1;动态数组是不要释放的。好像很好用,但是编译器释放它时还是会对每个元素置为nil的,而且是作为接口,仍然有非法地址访问错误的可能。可以这样  for i := Low(arr) to High(arr) doPointer(arr[i]) := nil;但是这毕竟是违反编码习惯的,而且每次使用都要记得作,不记得作也可能不会马上出错,所以有可能成为隐患。最后,就是使用TList。不过TList中是指针,所以Add时必须这样procedure XXX.Add(AIntf: I1)Begin    InterList.Add(Pointer(AIntf));End;由于它本来就保存的是指针,所以释放时也不需要特殊处理。好像比较完美,但是还是有一个陷阱,如果你写了这样的代码会怎么样呢?interList.Add(TC2.Create);或者Obj2 := TC2.Create;interList.Add(Obj2);错!因为保存的是纯粹的指针,所以转化为接口时,对象引用到接口引用的地址转换没有进行(它也不知道如何进行),所以调用接口声明的方法时就又是一个非法地址访问错误。只能这么写:interList.Add(Pointer(I1(TC2.Create)));虽然有些麻烦,相比之下这已是最佳方案(我所知的)了。因为你如果你忘记转会在第一次调用时就出错,比较容易发现错误(相比于使用array)。所以我建议:1,  使用Tlist来管理接口引用。2,  增加时要把对象转型成Interface再转型成Pointer,如果本来就是接口引用的话直接转为Pointer即可。3,  对于没有使用Tlist来管理的接口引用。对于接口的引用要用下面的方法手动置为nil:Pointer(IntfRef):= nil;另外,TInterfacedObject也实现了IInterface的三个方法。所以从它继承也可以省去自己实现这三个方法的麻烦。但是它的_Release是这样实现的:function TInterfacedObject._Release: Integer;begin  Result := InterlockedDecrement(FRefCount);  if Result = 0 then    Destroy;end;我们前边说过,接口引用的置nil会调用接口的_Release,所以有可能会把对象给释放掉,我当时可是被它吓了一跳,多亏我没用过它。也就是这样实现下比从TComponent继承带来更大的麻烦。除非特殊用途,不建议使用。上面对接口的所有讨论,只限于普通使用的语言级的接口,没有讨论Com+接口。Delphi的这些诡异的接口的实现,和它是从Com+接口发展而来是有很大关系的,也就是由于背负了过重的历史包袱而作出的妥协的结果。

来源:http://www.tulaoshi.com/n/20160219/1602200.html

延伸阅读
标签: Web开发
    最近学习PHP5接口的过程中遇到了困难 ,书中说是实现多重继承的一种方式,但我依然不知道具体该如何实现。在网上查PHP接口的资料很少,就查了java的,其实基本上都差不多。看完《澄清Java(接口与继承)》这篇文章才恍然大悟,原来我一开始理解就有误,所谓的多重继承是指接口继承类,而不是类继承接口。  &nb...
来自Delphi Help,关键字Interface types: overview 接口和类一样,只能在程序或单元的最外层被声明(也就是interface section,也就是全局可见的),不能在过程或函数中声明。接口类型的声明格式如下: type interfaceName = interface (ancestorInterface)  //关键字interface    ['{GUID}']  ...
Interfaces 接口 在软件工程中,由一份“契约”规定来自不同的开发小组的软件之间如何相互作用是非常常见的。每个小组都可以在不知道别的组的代码的前提下独立开发自己的代码。Java中的interface就是这样的一份“契约”。 举个例子,假设在未来社会有一种智能汽车,它可以在自动运载旅客而不需要人工操作。汽车生产商开发了软件(当然是用Java...
标签: Java JAVA基础
JDBC接口技术(07-29 10:13:45) 作者:郑林峰 张公忠 JDBC是一种可用于执行SQL语句的JavaAPI(ApplicationProgrammingInterface应用程序设计接口)。它由一些Java语言编写的类和界面组成。JDBC为数据库应用开发人员、数据库前台工具开发人员提供了一种标准的应用程序设计接口,使开发人员可以用纯Java语言编写完整的数据库应用程序。 一、OD...
Microsoft Agent具有相当广泛的用途,我们既可以把它加入到普通应用程序中供本地系统使用,也可以把它嵌入到HTML文档中供Internet/Intranet使用。Microsoft Agent支持C/C++、Visual Basic、Java、JScript和VBScript等多种编程语言,并为程序员提供了OLE自动化服务器和ActiveX控件两种编程方法,从本质上讲,这两种编程方法都属于OLE技术的范畴...

经验教程

524

收藏

11
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部