CnPack Forum


 
Subject: [原创] 查询接口小议
stanleyxu2005
新警察
Rank: 1



UID 5617
Digest Posts 1
Credits 45
Posts 15
点点分 45
Reading Access 10
Registered 2007-2-11
Status Offline
Post at 2007-8-2 12:06  Profile | Site | Blog | P.M. 
[原创] 查询接口小议

原文地址:http://blog.csdn.net/Stanley_Xu/archive/2007/08/02/1722313.aspx


前面的废话
接口大大增强了类设计的灵活性,类似于c++中的多重继承。不管你是否真正了解接口 (Interface),但它已经默默的在为你的程序服务了。你可以去看一下 TComponent 的定义部分,你会发现它内部已经封装了2个接口:IInterface,IInterfaceComponentReference不难发现,Delphi 中除了原子类 TObject 之外,任何类有且只有一个父类,但同时它可以拥有0..n个接口。接口是一组抽象的函数集,不能被实例化,函数实现部分必须由它的实现类或间接实现 (外包) 类完成。

如何查询接口
先请看下面的代码:
type
  IHello =
interface(IUnknown)
    [
'{1EE7A0AA-F525-4DD5-AB1B-900348BF8322}']
   
procedure Hello;
  
end;
  
  THello =
class(TObject, IHello)
   
// Implements IHello
   
procedure Hello;
  
end;

procedure UnSafeInftCall(Obj: TObject);
begin
  
// Case 1
  Obj.Hello;
//<-- Syntax error
  
// Case 2
  THello(Obj).Hello;
  
// Case 3
  IHello(Obj).Hello;
end;

procedure SafeInftCall(Obj: TObject);
var
  pIntfHello: IHello;
begin
  
// Case 4
  
if Obj.GetInterface(IHello, pIntfHello) then
    pIntfHello.Hello;
  
// Case 5
  
try
    pIntfHello := Obj
as IHello;
    pIntfHello.Hello;
  
except end;
  
// Case 6
  
if Supports(Obj, IHello, pIntfHello) then
    pIntfHello.Hello;
end;

前面3种情况都是不安全的接口函数调用,这里就不仔细说了。
情况4:这种方法是最直接的。GetInterface 函数会去 VMT 中寻找是否定义过 IHello 这个接口,如果找到的话,并且把实现者的实例返回到 pIntfHello 中。这样就可以安全的使用了。
情况5:这里使用了保留字 as 进行强制类型转换。如果转换失败运行期会丢出异常,我们可以通过 try…except 处理掉异常。其实 as 内部机制就是调用了 _IntfCast,只是比情况4 多了一个抛出异常而已。
情况6:可以通过Supports 函数查询接口。于情况4不同的是 Supports 会自动检查 Obj 是否是个有效的实例,帮你省了一行代码。(但是这个函数会让你在接口转换时付出其它额外的代价)

使用 Supports 2个问题
这个问题的发现实出偶然:网友许子健设计的一个接口应用中统一使用了 as 进行转换,而我当时推荐他使用 Supports,因为 Supports 在查询接口失败后并不抛异常,而是返回 False。虽然只是小小的代码改动,但是他的程序意外崩溃了。
请看下面的代码
type
  THelloImplementor =
class(TInterfacedObject, IHello)
  
public
   
procedure Hello;
  
end;

procedure TestMe;
var
  Obj: THelloImplementor;
begin
  Obj := THelloImplementor.Create;
  
try
   
if Supports(Obj, IHello) then //<-- Obj.Destroy is called
begin
      
//Own code
end
  
finally
    ShowMessage(Obj.ClassName);
//<-- Crashed!
    Obj.Free;
  
end;
end;

奇怪吗,为什么用 Supports 查询接口出错了呢?通过调试发现,在执行Supports 之后,Obj 的实例被意外的释放了。于是乎意外应该是在 Supports 之内发生的。现在我们来看一下Supports 的实现:
function Supports(const Instance: TObject; const IID: TGUID): Boolean; overload;
var
  Temp: IInterface;
begin
  Result := Supports(Instance, IID, Temp);
end;

当执行 Result := Supports(Instance, IID, Temp) 时,Temp 这个接口指针指向 Instance (Obj),同时因为接口引用计数的关系Obj.FRefCount 加一。当离开Supports 函数时本地变量指针Temp 会被清除于是Obj.FRefCount 减一。这个加一减一表面上没有差别,但是你完全无法预计 Instance 内部会对 FRefCount 的变化做什么样的处理。而恰恰 Obj 是从臭名昭著的 TInterfacedObject 继承来的。这个类会当 FRefCount=0 时释放掉实例本身。这个就是该程序出错的真正原因。

好了,下面再介绍一个隐藏的比较深的问题。这个和接口的委托机制有关。请看下面的代码
type
  TVirtualImplementor =
class(TInterfacedObject{TObject does not have problem}, IHello)
  
public
    FImplementorOfIHello: THelloImplementor;
   
property ImplementorOfIHello: THelloImplementor readFImplementorOfIHello implements IHello; //<--Be careful!
  
end;

procedure TestMeAgain;
var
  VI: TVirtualImplementor;
  pIntfHello: IHello;
begin
  VI := TVirtualImplementor.Create;
  
try
   
// Method 1
   
try
      pIntfHello := VI
as IHello;
      pIntfHello.Hello;
   
except end;
   
// Method 2
   
if Supports(VI, IHello, pIntfHello) then //<-- VI.Destroy is called
      pIntfHello.Hello;
  
finally
    ShowMessage(VI.ClassName);//<-- Crashed!
    VI.Free;
  
end;
end;

如果使用 as 做类型转换,程序是可以顺利运行的。但是为什么用Supports 就出错了呢?我们应该会很自然的联想上面那个问题。但问题是,这次接口指针 pIntfHello实实在在地获得了接口,而且在 VI 释放之前并没有清除,也就是说VI 不应该同上面的情况一样被自动销毁的。那么我们就再看一下 Supports 的实现:
function Supports(const Instance: TObject; const IID: TGUID; out Intf): Boolean; overload;
var
  LUnknown: IUnknown;
begin
  Result := (Instance <>
nil) and
            ((Instance.GetInterface(IUnknown, LUnknown)
and Supports(LUnknown, IID, Intf)) or //<-- RefCount changed!
             Instance.GetInterface(IID, Intf));
end;

倒数第三行的地方,Supports 并不是直接通过 Instance去查询是否支持IHello接口的而是先去查询Instance 是否支持 IUnknown,然后通过IUnknown 去查询 IHello接口。我不清楚 Delphi 为什么这样处理。
现在我们手动调试程序:
当执行Instance.GetInterface(IUnknown,LUnknown) 之后,LUnknown 指针指向了 Instance ( VI),同时因为接口引用计数的关系VI.FRefCount 加一。
然后程序继续执行Supports(LUnknown, IID, Intf)。这时 Intf 指针指向了IHello 的真正实现者 VI.FImplementorOfHello,同样的的,VI.FImplementorOfHello.FRefCount 加一。
当查询成功离开Supports 函数时,本地变量指针 LUnknown 会被清除,于是 VI.FRefCount 减一。不巧的是 VI 也是由 TInterfacedObject 继承来的……。种种不幸最终酿成又一场惨祸。

最后总结
这篇文章除了要介绍一下接口的查询方法外,主要是要想交代一下我在具体使用接口中发现的一些问题。上述代码中包涵了3个问题:
1.     TInterfacedObject 由于会在 FRefCount=0 时释放掉对象实例,所以在使用上要格外小心。建议重新封装一个TInterfacedObjectEx,或者改用 TComponent
2.     Supports 内部这行代码虽不知其用意,但显然是不安全的!尤其是在使用委托机制实现接口封装的时候。说明:我暂时无法证明去掉有问题的这行是否能保证不引入其它问题。
3.     上述代码设计上的问题是:既然 TVirtualImplementor 把接口的实现工作委托 (外包) 给了 TRealImplementor,那么TVirtualImplementor 就应该定义成 TObject。尽管程序可以运行,但是逻辑上还是有些不通。
此外,委托 (implements) 这个概念挺有意思,它提高了接口的复用度。有时间的话,我会详细再写一篇介绍。




[ 本帖最后由 stanleyxu2005 于 2007-8-3 23:44 编辑 ]




http://getgosurf.com/forum/
Top
jAmEs_
灌水部部长
Rank: 8Rank: 8



Medal No.1  
UID 886
Digest Posts 0
Credits 1134
Posts 600
点点分 1134
Reading Access 10
Registered 2005-6-5
Location 广东
Status Offline
Post at 2007-8-2 17:14  Profile | Blog | P.M. 
implements这个有点意思好像,以前没有用过,希望讲讲~~
Top
zzzl (早安的空气)
版主
Rank: 7Rank: 7Rank: 7



UID 590
Digest Posts 0
Credits 399
Posts 199
点点分 399
Reading Access 100
Registered 2004-11-29
Status Offline
Post at 2007-8-2 22:13  Profile | Blog | P.M.  | QQ
晕,本版块为最强技术含量的水园,鉴定完毕
Top
xwing
新警察
Rank: 1



UID 4858
Digest Posts 0
Credits 19
Posts 7
点点分 19
Reading Access 10
Registered 2007-1-22
Status Offline
Post at 2007-10-22 19:54  Profile | Blog | P.M. 
反正我不会同时混用接口和对象,问题多的很.实在要用,就让它不能自动引用计数.
Top
 




All times are GMT++8, the time now is 2024-4-27 10:55

    本论坛支付平台由支付宝提供
携手打造安全诚信的交易社区 Powered by Discuz! 5.0.0  © 2001-2006 Comsenz Inc.
Processed in 0.019413 second(s), 7 queries , Gzip enabled

Clear Cookies - Contact Us - CnPack Website - Archiver - WAP