如何释放接口对象 (Delphi 7)

Posted

技术标签:

【中文标题】如何释放接口对象 (Delphi 7)【英文标题】:How to Free an Interface Object (Delphi 7) 【发布时间】:2011-06-16 19:25:14 【问题描述】:

在我的应用程序的某些部分,我收到一个我知道是对象的接口,尽管我不知道确切的类。我必须将该对象存储在接口类型变量中。

最终,我可能会收到另一个该类型的实例,并且必须丢弃第一个实例并用新实例替换。为此,我需要释放接口对象使用的内存(我的接口提供了 AsObject 方法,因此我可以在其上使用 TObject 方法)。我的问题是,当我想再次将“nil”分配给该变量时,我遇到了访问冲突。

我写了一个小程序来重现我的情况。我在这里发布它以澄清情况。

program Project1;

$APPTYPE CONSOLE

uses
  SysUtils, Classes;

type
   ISomeInterface = interface
      function SomeFunction : String;
      function AsObject : TObject;
   end;

   TSomeClass = class(TComponent, ISomeInterface)
   public
      called : Integer;
      function SomeFunction : String;
      function AsObject : TObject;
   end;

var
   SomeInterface : ISomeInterface;
   i : Integer;

function TSomeClass.SomeFunction : String;
begin
   Result := 'SomeFunction called!';
end;

function TSomeClass.AsObject : TObject;
begin
   Result := Self;
end;

begin
   try
      SomeInterface := nil;

      for i := 1 to 10 do
      begin

         if SomeInterface <> nil then
         begin
            SomeInterface.AsObject.Free;
            SomeInterface := nil;          // <-- Access Violation occurs here
         end;

         SomeInterface := TSomeClass.Create(nil);
         SomeInterface.SomeFunction;       // <-- if commented, Access 
                                           //     Violation does not occur

      end;

   except on e : Exception do
      WriteLn(e.Message);
   end;

end.

所以问题是:如何正确释放该对象?

【问题讨论】:

【参考方案1】:

假设您有这样做的正当理由(并且使用 TComponent 很有可能这样做 - 请参阅答案末尾的原因),那么问题是由于在您之后更改接口变量的引用而发生的已销毁它当前引用的对象。

对接口引用的任何更改都会生成如下代码:

  intfA := intfB;

变成(简单来说):

  if Assigned(intfA) then
    intfA.Release;

  intfA := intfB;

  if Assigned(intfA) then
    intfA.AddRef;

如果您将其与您的代码相关联,您应该会看到问题:

  SomeInterface.AsObject.Free;
  SomeInterface := nil;  

变成:

SomeInterface.AsObject.Free;

if Assigned(SomeInterface) then
  SomeInterface.Release;

SomeInterface := nil;  

if Assigned(SomeInterface) then
  SomeInterface.AddRef;

因此,您可以看到生成的对 Release() 的调用是由于将 NIL 分配给导致访问冲突的接口而产生的。

您还应该很快看到有一种简单的方法可以避免这种情况,只需将对象的释放推迟到您将接口引用设为 NIL 之后:

obj := SomeInterface.AsObject;
SomeInterface := NIL;
obj.Free;

但是

这里的关键问题是,为什么要显式释放接口(并且可能是引用计数)的对象。

当您更改代码以缓存对象引用并在显式释放对象之前对接口进行 NIL 时,您可能会发现 obj.Free 将导致访问冲突,因为接口引用的 NIL 本身可能会导致在被释放的对象中。

确保显式释放接口对象是安全的唯一方法是:

1) 接口对象已覆盖/重新实现 IUnknown 并消除了引用计数生命周期管理

2) 您在代码的其他地方没有对该对象的其他接口引用。

如果这些条件中的第一个对您来说没有多大意义,那么您不希望高高在上,这可能是一个好兆头,表明您不应该显式释放对象,因为它几乎肯定是通过引用管理的数。

话虽如此,既然您使用的是接口 TComponent 类,那么只要您的 TComponent 类不封装 COM 对象,那么 TComponent 就满足条件 #1,所以剩下的就是确保您的代码的其余部分满足条件#2。

【讨论】:

这是不正确的,因为TComponent 不使用引用计数; AddRefRelease 返回 -1 非常感谢,Deltics。您关于反转释放-nilling 代码的建议听起来可以解决我的问题。这是对界面如何工作的深刻见解,我从您的回答中学到了很多东西。再次感谢。 +1。尽管您可以在类中禁用引用计数(通过提供根本不计算任何内容的 _AddRef_Release 的实现,但您不能禁用编译器生成对引用计数函数的 调用 . 而且由于函数总是会被调用,所以你需要确保没有剩余的引用你销毁接口变量引用的对象之前,即使你不打算这样做计数引用。 Rob 所说的就是我在回答中所解释的! ;) @deltics 我很想听听更多关于 TInterfacedObject 析构函数中的错误。【参考方案2】:

您不应该使用 TComponent 作为接口对象的基类,而应该使用 TInterfacedObject。 TInerfacedObject 已经实现了必要的函数来处理 Delphi 中接口的生命周期管理。你也不应该将你的接口作为接口和对象混合访问。这是对您的代码的修改,它工作得很好,没有内存泄漏。

program Project2;
$APPTYPE CONSOLE

uses
    SysUtils, Classes;

type
    ISomeInterface = interface
        function SomeFunction: string;
    end;

    TSomeClass = class(TInterfacedObject, ISomeInterface)
    public
        called: Integer;
        function SomeFunction: string;
    end;

var
    SomeInterface: ISomeInterface;
    i: Integer;

function TSomeClass.SomeFunction: string;
begin
    Result := 'SomeFunction called!';
end;

begin
    try
        SomeInterface := nil;
        for i := 1 to 10 do
        begin
            if SomeInterface <> nil then
            begin
                SomeInterface := nil;
            end;
            SomeInterface := TSomeClass.Create;
            SomeInterface.SomeFunction;
        end;
    except
        on e: Exception do
            WriteLn(e.message);
    end;
end.

【讨论】:

我不同意“永远不要将访问您的界面作为界面和对象混合使用”。如果您的对象在没有引用计数和生命周期管理的情况下实现 IInterface,则可以这样做。事实上,TComponent 就是这样做的。 是的。意味着永远不要混淆生命周期管理原则。 TXMLDocument 就是一个很好的例子。将其用作接口 (IXMLDocument) 或将其用作 TXMLDocument(具有所有者)。将一个 TXMLDocument 分配给一个接口,并且免费调用您将在 IXMLDocument 版本上获得访问冲突。 感谢您的提示。可悲的是,这是我要维护的遗留代码,每个类都是 TComponent,目前无法更改它。还是谢谢你的回答,学习了。 @David,即使您的课程不计算引用,仍然必须计算它们,以便您可以确定在调用 Free 时没有引用保留物体上。 (您不必在代码中计算它们;相反,您可以在办公桌旁的记事本中对它们进行计数,并在每次编译项目之前检查它们。)由于编译器将插入调用无论如何引用计数功能,我说你不妨让类自己跟踪计数。 有点像@David。接口的问题更为明显。使用普通的对象引用,您实际上不必在释放对象之前清除所有引用。您只需要确保不使用剩余变量的剩余值。但是对于接口,即使 programmer 不再使用陈旧的接口变量,compiler 也会在变量超出范围或重新生成时隐式使用它-分配。由于问题是由您看不到的代码触发的,因此更容易忘记它潜伏并导致崩溃。【参考方案3】:

你正在混合它。一切都取决于_AddRef_Release 方法。检查system.pas 中的TInterfacedObject 是如何声明的。

Delphi 在使用Interafaces 时只调用_AddRef 和_Release 方法,调用Free 取决于对象如何实现 _Relase 方法。

TComponent 不会自动销毁(Com 对象组件除外)。

var
  o: TSomeClass;
begin
  ..
  ..
  begin
    o := SomeInterface.AsObject as TSomeClass;
    SomeInterface := nil;  // now we decrease reference counter, but it will not free anything unless you rewrite _AddRef and _Release methods
    o.Free; // just free object by your own
 end;

【讨论】:

+1 你说得对,虽然有点迟钝,但 TComponent 实现 IInterface 是没有引用计数的。 实际上并不是在减少引用计数器,而是在确保不再有引用。如果还有任何引用,那么编译器将尝试在它们上调用_Release(不管对象是否“使用”引用计数),这将失败,因为底层对象已经被销毁。 @rob 这消除了我的误解,谢谢。我的非参考计数 addref/release 方法不会触及自我并且是免疫的。 TComponent 中确实触及 Self 的实现是一个定时炸弹!【参考方案4】:

当您有一个接口变量(例如 ISomeInterface var)时,您不需要释放它,因为它是引用计数的,并且会在它超出范围时释放它自己。

阅读 Rob Kennedy 对这个问题的回答: Delphi7, passing object's interface - causes Invalid Pointer Operation when freeing the object

来自http://delphi.about.com/od/beginners/l/aa113004a.htm

界面一出 范围,Delphi 实际上会释放 界面自动为您服务! 接口在一个内声明 程序或函数自然会 当程序超出范围时 结束。接口在一个内声明 类或全局声明将 当 对象被释放或程序结束。

如果有疑问,请尝试使用 FastMM 内存管理器并调整内存泄漏检测,以查看对象是否泄漏。

【讨论】:

这是不正确的,因为TComponent 不使用引用计数; AddRefRelease 返回 -1 我不完全知道原因,但实际上,我在类中添加了一个析构函数 Destroy 并尝试删除引用而不释放它。析构函数从未被调用。 @pablo 在发布的代码中,唯一对您的对象调用 free 的是您对 free 的明确调用。 TComponent 上没有引用计数。 我理解这一点,因为许多答案都提到 TComponent 不保留引用计数。我知道我必须自己释放该对象(我不知道如果我创建了 TSomeClass 并拥有一个以后可以释放的所有者,我是否还需要这样做)。 如果您为TSomeClass 实例分配所有者,那么所有者稍后将释放该对象。在这种情况下,您不需要调用Free(尽管调用它是无害的——对象会通知其所有者它已被销毁)。

以上是关于如何释放接口对象 (Delphi 7)的主要内容,如果未能解决你的问题,请参考以下文章

【delphi线程】 如何自动释放?

delphi 7 superobject控件如何遍历子对象

Delphi采用接口实现DLL调用

Delphi,如何在 TList 中释放记录

如何使用 IDisposeable 接口释放 excel 对象

delphi释放窗体后如何再次载入窗体