接口大大增强了类设计的灵活性,类似于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;
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;
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 <> 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 继承来的……。种种不幸最终酿成又一场惨祸。
(1) TInterfacedObject 由于会在 FRefCount=0 时释放掉对象实例,所以在使用上要格外小心。建议重新封装一个TInterfacedObjectEx,或者改用 TComponent。
(2) Supports 内部这行代码虽不知其用意,但显然是不安全的!尤其是在使用委托机制实现接口封装的时候。说明:我暂时无法证明去掉有问题的这行是否能保证不引入其它问题。
(3) 上述代码设计上的问题是:既然 TVirtualImplementor 把接口的实现工作委托 (外包) 给了 TRealImplementor,那么TVirtualImplementor 就应该定义成 TObject。尽管程序可以运行,但是逻辑上还是有些不通。