查询接口小议

前面的废话

接口大大增强了类设计的灵活性,类似于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 read FImplementorOfIHello implements IHello; //<-- Be careful!
  
end;

procedure TestMe;
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 <> 
niland
            ((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) 这个概念挺有意思,它提高了接口的复用度。有时间的话,我会详细再写一篇介绍。
 

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试