Delphi TObject在对象销毁时为接口字段的清除提供了什么保证?

Delphi TObject在对象销毁时为接口字段的清除提供了什么保证?,delphi,interface,delphi-xe2,Delphi,Interface,Delphi Xe2,下面是一些示例代码,它是Delphi中的一个独立控制台应用程序,它创建一个对象,然后创建一个对象,该对象是TInterfacedObject,并将接口引用分配给ToObject中的一个字段: program ReferenceCountingProblemProject; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; type ITestInterface = interface ['{A665E2EB-183C-442

下面是一些示例代码,它是Delphi中的一个独立控制台应用程序,它创建一个对象,然后创建一个对象,该对象是
TInterfacedObject
,并将接口引用分配给ToObject中的一个字段:

program ReferenceCountingProblemProject;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils;
type
  ITestInterface = interface
     ['{A665E2EB-183C-4426-82D4-C81531DBA89B}']
     procedure AnAction;
  end;
  TTestInterfaceImpl = class(TInterfacedObject,ITestInterface)
     constructor Create;
     destructor Destroy; override;

     // implement ITestInterface:
         procedure AnAction;
  end;

  TOwnerObjectTest = class
     public
         FieldReferencingAnInterfaceType1:ITestInterface;
   end;
   constructor TTestInterfaceImpl.Create;
   begin
     WriteLn('TTestInterfaceImpl object created');
   end;
   destructor TTestInterfaceImpl.Destroy;
   begin
     WriteLn('TTestInterfaceImpl object destroyed');
   end;
   procedure TTestInterfaceImpl.AnAction;
   begin
       WriteLn('TTestInterfaceImpl AnAction');
   end;
procedure Test;
var
  OwnerObjectTest:TOwnerObjectTest;
begin
  OwnerObjectTest := TOwnerObjectTest.Create;
  OwnerObjectTest.FieldReferencingAnInterfaceType1 := TTestInterfaceImpl.Create as ITestInterface;
  OwnerObjectTest.FieldReferencingAnInterfaceType1.AnAction;
  OwnerObjectTest.Free;      // This DOES cause the clearing of the interface fields automatically.
  ReadLn; // wait for enter.
end;
begin
   Test;
end.
我之所以写这段代码,是因为我不确定,在一些琐碎的例子中,Delphi是否总是清除我的接口指针。以下是程序运行时的输出:

TTestInterfaceImpl object created
TTestInterfaceImpl AnAction
TTestInterfaceImpl object destroyed
这是我非常希望看到的结果。我之所以编写这个程序,是因为我看到我正在开发的一个大型Delphi应用程序违反了“我和Delphi之间的合同”。我看到的对象不会被释放,除非我在析构函数中明确地将它们归零,如下所示:

 destructor TMyClass.Destroy;
 begin
        FMyInterfacedField := nil; // work around leak.
 end;
我相信Delphi正在尽最大努力将这些接口归零,因此,当我在上面的测试代码中的析构函数上设置断点时,我得到了以下调用堆栈:

ReferenceCountingProblemProject.TTestInterfaceImpl.Destroy
:00408e5f TInterfacedObject._Release + $1F
:00408d77 @IntfClear + $13
ReferenceCountingProblemProject.ReferenceCountingProblemProject
正如您所看到的,正在生成对
@IntfClear
的调用,但上面的调用堆栈中缺少“Free”,这让我有点困惑,因为这两个调用似乎是因果关联的,而不是直接在彼此的调用路径中。这向我表明,在调用析构函数
TObject.Free
之后的某个时刻,编译器本身在我的应用程序中发出一个
@IntfClear
。我读对这个标志了吗

我的问题是:Delphi的TObject是否总是保证接口类型字段的终结?如果没有,什么时候我的界面会被清除,什么时候我必须手动清除它?接口引用的最终确定是作为TObject的一部分实现的,还是作为某些通用编译器范围语义的一部分实现的?事实上,我应该遵循什么规则,什么时候手动调零一个接口,什么时候让Delphi帮我调零?想象一下,在我的应用程序中,有200多个类将接口存储为字段。我是否在析构函数中将它们全部设置为零?我如何决定做什么

我的怀疑是,要么(a)TObject提供了这种保证,附带条件是,如果您做了一些愚蠢的事情,并且不知何故没有开始调用TObject.Destroy,那么在包含接口引用字段的对象上,这两个字段都会泄漏,要么(b)低于TObject级别的编译器提供了这种语义保证,在超出范围的层面上,正是这一面让我抓狂,无法解释现实世界中可能遇到的复杂场景

对于普通情况,比如我删除
OwnerObjectTest.Free
更新通过单步执行并声明我自己的析构函数,我能够获得不同的调用堆栈,这更有意义:

ReferenceCountingProblemProject.TTestInterfaceImpl.Destroy
:00408e5f TInterfacedObject._Release + $1F
:00408d77 @IntfClear + $13
:00405483 TObject.Free + $B
ReferenceCountingProblemProject.ReferenceCountingProblemProject

这似乎表明
@IntfClear
是由
TObject.Free
调用的,这正是我非常希望看到的。

当执行对象的析构函数时,对象实例的所有字段都已完成。这是由运行时保证的。事实上,所有托管类型的字段都在销毁后最终确定

此类参考计数对象未被销毁的可能解释如下:

  • 析构函数未被执行,或者
  • 其他内容包含对该对象的引用

  • 原则上同意。然而,在实践中,我认为备选方案3可能存在。内存写入错误覆盖字段而不取消设置
    IUnknown
    refcount,导致引用为1的泄漏。如果我的hack(在早期明确为零)修复了我的bug,它可能只是使后来的内存随机写入无害,而不是真正修复我的大型现实世界应用程序中随机写入内存的根本原因。至少如果我知道这是我可以隔离它的剩余可能性。如果在销毁中发生异常怎么办?比如说,我有Intf1和Intf2字段,清除Intf1会引发什么?那么Intf2会被跳过吗?@Arioch'析构函数抛出是一个错误。在这一点上,所有的赌注都被取消了。@Arioch'The,但是是的,例外情况会向上浮动,跳过所有的东西。这一点很好。异常的错误计时是此顺序中可能的额外“混乱”输入。您是否可以覆盖泄漏类的_AddRef和_Release来记录调用,然后查看它们是否平衡?还请注意,同一对象的不同接口可能(尽管不应该)具有不同的_addRef和_Release主体。您不应该期望看到TObject.Free直接调用IntfClear。您可以在System.pas中看到,TObject.Free调用的唯一函数是TObject.Destroy。编译器魔术使析构函数调用ClassDestroy。它调用TObject.FreeInstance,它调用TObject.CleanupInstance,它调用FinalizerRecord,它调用FinalizerRay,这就是所谓的IntfClear。所有这些函数都从调用堆栈中省略了,这是由于优化或RTL使用调用约定时快时慢,调试器无法进行补偿。调试器在无法识别所使用的调用约定时会省略堆栈帧。我急切地希望看到一个可重现的情况,在这种情况下,您实际上必须将接口引用设置为
    nil
    。在此之前,请遵循Arhioch的建议:确保您记录
    \u AddRef
    \u Release
    调用,以进一步放大查看发生了什么。我相信我可以通过获取一个引用对象并通过丑陋的
    FillChar(PAnsiChar(p),…)
    黑客将其置零来复制它