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

16
在我的应用程序的某些部分,有这样的情况:我接收到一个接口,我知道它是一个对象,但我不知道确切的类。我必须将该对象存储在接口类型变量中。
最终,我可能会收到该类型的另一个实例,并且第一个实例必须被丢弃并替换为新实例。为此,我需要释放接口对象使用的内存(我的接口提供了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.
所以问题是:我如何正确释放那个对象?
4个回答

31

假设你有一个合理的理由这么做(使用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;

因此,你可以看到将NIL分配给接口所导致的生成的调用Release()是导致访问冲突的原因。

你也应该很快看到有一种简单的方法可以避免这种情况,只需要在将接口引用设置为NIL后延迟释放对象即可:

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

但是

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

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

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

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

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

如果第一个条件对您来说不太有意义,那么,不想居高临下,这很可能是您不应该显式释放对象的一个很好的迹象,因为它几乎肯定是由引用计数进行管理。

话虽如此,由于您正在使用一个接口化的TComponent类,只要您的TComponent类不封装COM对象,那么TComponent就符合条件#1,那么剩下的就是确保您的代码满足条件#2。


1
这是不正确的,因为TComponent没有使用引用计数;AddRefRelease返回-1。 - David Heffernan
2
非常感谢,Deltics。你关于反转释放-清零代码的建议听起来像是能解决我的问题。这也让我对接口的工作原理有了更深入的了解,我从你的回答中学到了很多。再次感谢。 - Pablo Venturino
4
尽管你可以在类中禁用引用计数(通过提供 _AddRef_Release 的实现来不对其进行计数),但无法禁用编译器生成对引用计数函数的调用。由于这些函数总是会被调用,因此在销毁接口变量所引用的对象之前,即使你不打算计数引用,仍需要确保没有剩余引用。 - Rob Kennedy
1
Rob所说的就是我在答案中解释的内容!;) - Deltics
2
@deltics,我很想听听关于TInterfacedObject析构函数中的错误的更多信息。 - David Heffernan
显示剩余22条评论

6

您不应该将TComponent作为接口对象的基类,而应该使用TInterfacedObject。 TInterfacedObject已经实现了在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 就是这样做的。 - David Heffernan
1
是的。意味着永久管理原则不要混淆。TXMLDocument是一个很好的例子。可以将其用作接口(IXMLDocument)或使用它作为TXMLDocument(带有owner)。将TXMLDocument分配给接口,然后调用free时,您将在释放IXMLDocument时获得访问冲突。 - Mikael Eriksson
谢谢您的建议。不幸的是,这是我正在维护的遗留代码,每个类都是TComponent,并且目前无法更改。仍然感谢您的答案,我从中学到了东西。 - Pablo Venturino
@David,即使你的类不计算引用,仍然必须计算它们,以便在调用对象的Free时可以确定没有引用留下。 (你不必在代码中计算它们;相反,你可以在桌子旁边的记事本上记录它们,并在编译项目之前每次检查。)由于编译器无论如何都会插入对引用计数函数的调用,所以我认为你最好让类自己跟踪计数。 - Rob Kennedy
@rob 即使你不使用接口,这也是正确的。对于所有非垃圾回收环境都是如此。 - David Heffernan
3
有点是这样的,@David。与普通对象引用不同,接口存在的问题更加突出。你实际上不需要在释放对象之前清除所有引用,只需要确保不再使用剩余变量的残留值就可以了。但对于接口来说,即使程序员不再使用旧的接口变量,当变量超出作用域或重新分配时,编译器仍会隐式地使用它。由于该问题是由你看不到的代码触发的,因此很容易忘记它潜伏并导致崩溃。 - Rob Kennedy

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内存管理器并打开内存泄漏检测,以查看对象是否泄漏。

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

3
您正在混淆它。一切都取决于_AddRef和_Release方法。请查看system.pas中的TInterfacedObject声明方式。
在使用接口时,Delphi只会调用_AddRef和_Release方法,而Free的调用则取决于对象如何实现_Release方法。 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;

您说得没错,尽管措辞有些晦涩,TComponent 实现 IInterface 并没有进行引用计数。 - David Heffernan
并不是真正地减少引用计数,而是确保没有更多的引用。如果还有任何引用,编译器将尝试在它们上调用“_Release”(无论对象是否正在使用引用计数),这将失败,因为底层对象已经被销毁。 - Rob Kennedy
@rob,这解决了我的误解,谢谢。我的非引用计数AddRef/Release方法不涉及self并且是免疫的。在TComponent中涉及Self的实现是一个定时炸弹! - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接