工具软件   办公软件   操作系统   网络安全   设计在线   程序开发   教程宝典   软件下载   软件论坛
您的位置:软件 > 开发者网络 > 开发工具 > 开发专栏 > Delphi > 正文
Delphi接口编程的两大陷阱
[文章信息]
作者: kiol
时间:2004-09-24
出处:csdn
责任编辑:方舟
[文章导读]
能把一个对象引用强制转换成这个引用的类型没有声明实现的接口
advertisement
热点推荐
· 真没想到VB也可以这样用之指针技术
· 禁止QQ登录的方法
· 给你的XML文件做个数字签名
· ImageReady制作“焰火”小动画
· Java加密和数字签名编程快速入门
[正文]
  陷阱一、接口的类型转换陷阱

  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什么时候用以及怎么用这三个方法?所以我也不知道怎么实现这三个方法。

  如果不想自己实现这三个方法,你可以使用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;
try
Intf1 := OBJ2;
Intf1.DO;
Finally
OBJ2.Free;
Intf1 := nil;
End;

  会怎么样呢?会出非法地址访问错误。为什么?上面说过把接口引用设为nil时,会调用_IntfClear,而_IntfClear又会调用对象的_Release,而这时这个对象已经释放了,自然就出非法地址访问错误啦。

  有人说多此一举吗,接口引用只是个地址,没必要手动设为nil。

OBJ2 := TC2.Create;

try

Intf1 := 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 do
Pointer(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) do
Pointer(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+接口发展而来是有很大关系的,也就是由于背负了过重的历史包袱而作出的妥协的结果。



天极社区邀请您:写博客日记  上传相片   论坛聊天  订阅电子杂志  推荐网摘   免费图铃工具
笔名:   请您注意:

 遵守国家有关法律、法规,尊重网上道德,承担一切因您的行为而直接或间接引起的法律责任。

 天极网拥有管理笔名和留言的一切权利。
评论:
 
发表评论推荐给朋友我想参加相关培训打印我对此感兴趣订阅电子杂志
相关内容焦点新闻
  • 基于Delphi的组件设计之简单实例
  • 基于Delphi的组件设计之概念
  • 浅述Delphi下的OpenGL图形开发
  • 深入理解Delphi的消息机制
  • Delphi处理SQL Server多媒体数据
  • 民营家电商排队造手机 设备商全面杀入
  • 英特尔澄清杨旭任职传闻 官方没宣布此消息
  • 国资委河北密制联通拆分方案
  • 垃圾邮件害人害企害国 清除垃圾邮件不手软
  • 中兴携手阿尔卡特 全球逐鹿CDMA
  • 用友总裁王文京:誓将ERP变成“大众消费”
  • 香港消费者委员会:数码相机最贵未必最好
  • 外电称中兴正评估西门子手机业务 或能并购
  • Advertisement

    天极无线


    奇妙科幻|美好风光|清风车影|漫画卡通|星座生肖|明星写真|动物世界
    老鼠爱大米
    挥着翅膀的女孩
    女人味
    栀子花开
    白月光
    刚刚好
    江南
    快乐崇拜
    亲爱的你怎么不在我身边
    小薇
    2002年的第一场雪
    有多少爱可以重来
    我的地盘
    七里香
    情人
     
    老鼠爱大米 老板电话
    冲动的惩罚 七里香
    我不是黄蓉 女生撒娇
    盛夏的果实 坚持到底
    孤单北半球 眉飞色舞
    挪威的森林 可爱女人
    最浪漫的事 老板电话

    CSEEK搜索