释放接口对象

4

我有一个关于释放接口对象的疑问。

我的接口看起来像这样:

type IBase = interface(IInterface)
    function test: Boolean;  
end;

这个其他的类有一些属性,我希望在所有接口类中都可以使用:

type TBase = class(TInterfacedObject)
    protected
        FQuery: TADOQuery;
        FADOConnection: TADOConnection;
    public
        constructor Create; virtual; abstract;
        destructor Destroy;
    end;

我有一些继承之前类并实现接口的类。

type TExample= class(TBase, IBase)
    public
        function test: Boolean;
        destructor Destroy;
end;

所以,使用这种模式,我可以像下面这样使用类:
procedure someProcedure(aux : IBase);
begin
    aux.test; //Aux is an instance of TExample and I'm using the interfaced method
end;

我的问题是,我如何销毁这个IBase接口的辅助对象?我已经尝试了一些方法,首先检查它不是一个空对象:

(aux as TObject).Destroy; //Invalid Pointer operation
(aux as TInterfacedObject).Destroy; //Invalid Pointer operation
(aux as TExample).Destroy; Also invalid Pointer operation!!??

我看到有人写道,因为该对象继承自TInterfacedObject并实现了接口,所以不能直接释放该对象,而应该使用以下方式:

aux := nil;

引用计数器会自动处理,但在我的项目中使用ReportMemoryLeaksOnShutdown := True;会导致一些泄漏并且代码永远无法到达析构函数的断点。

我有什么遗漏吗?

编辑

我修改了我的构造函数,现在像这样:

type TBase = class(TInterfacedObject)
    protected
        FQuery: TADOQuery;
        FADOConnection: TADOConnection;
    public
        constructor Create; 
        destructor Destroy; override;
    end;


type TExample= class(TBase, IBase)
    public
        function test: Boolean;
end;

现在我猜这有更多意义,因为TBase正在分配和释放对象,而TExample类继承了析构函数。


3
你需要在析构函数声明中添加override;。除此之外,你不需要进行任何显式的操作来释放对象。 - 500 - Internal Server Error
那就像问一个问题一样,我会给你正确的答案...谢谢! - Izuel
这还不够。它仍然会失败。 - Jens Borrisholt
我现在正在执行 aux := nil 并进入 TExample 的销毁,这正是我想要的,我会检查内存泄漏,但不应该有任何问题。 - Izuel
3个回答

4
不要手动释放接口对象!当一个类派生自 TInterfacedObject 类时,它会自动进行引用计数,并且只要没有剩余的接口引用,就会自动释放。这意味着下面这个过程通过接口 IBase 引用实例 TExample。当 Obj 变量从堆栈中删除时,它会自动清除内存。
procedure Foo;
var
  Obj: IBase;
begin
  Obj := TExample.Create; // reference count will be set to 1 
  Obj.test;
end; // reference count will be set to 0 and Obj will be freed 

下一个过程引用了 TExample 的实例,而不是它的类名。这里不会进行引用计数。编译器不包含对 _AddRef_Release 的调用。因此,这个过程会泄漏内存:
procedure Foo;
var
  Obj: TExample;
begin
  Obj := TExample.Create; 
  Obj.test;
end; // Obj will not be freed automatically

因此,您需要自己清理堆,就像这样:

procedure Foo;
var
  Obj: TExample;
begin
  Obj := TExample.Create; 
  try
    Obj.test;        
  finally
    Obj.Free;
  end;
end; 

它可以工作,但是当Obj被传递时可能会很危险。一旦对象的引用存储在接口引用中,就会出现问题。让我们看看这个例子:

procedure Bar(Obj: IBase);
begin
  //...
end;

procedure Foo;
var
  Obj: TExample;
begin
  Obj := TExample.Create; 
  try
    Bar(Obj); 
    Obj.test; // Access violation!       
  finally
    Obj.Free;
  end;
end; 

这里发生了什么?

Obj被创建并存储为类引用。引用计数为0。当调用Bar(Obj)时,该对象将被存储在接口引用中。编译器在调用Bar时包括对_AddRef_Release的调用。引用计数器将增加和减少,因此它再次变为0,并且对象自动销毁。

如何处理它?

  • 不要手动释放接口对象!
  • 让引用计数为您完成工作。
  • 不要通过其具体类存储TInterfacedObject的派生。
  • 只通过它们实现的接口来存储它们。
  • 避免接口对象之间的循环引用。Delphi中没有垃圾回收器!

1

你的示例有些奇怪。

由于你已经将TBase构造函数声明为virtual abstract,因此必须在TExample类中声明一个构造函数。

正如其他人所说,你必须向析构函数添加override指令。

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  FastMM4,
  System.SysUtils, ADODB, ActiveX;

type
  IBase = interface(IInterface)
    function test: Boolean;
  end;

  TBase = class(TInterfacedObject)
    protected
      FQuery: TADOQuery;
      FADOConnection: TADOConnection;
    public
      constructor Create; virtual; abstract;
      destructor Destroy; override;
  end;

  TExample= class(TBase, IBase)
    public
      function test: Boolean;
      constructor Create; reintroduce; virtual;
      destructor Destroy; override;
  end;

{ TBase }

destructor TBase.Destroy;
begin
  FQuery.Free;
  FADOConnection.Free;
  inherited;
end;

{ TExample }

constructor TExample.Create;
begin
  //inherited;
  FADOConnection := TADOConnection.Create(nil);
  FQuery := TADOQuery.Create(nil);
end;

destructor TExample.Destroy;
begin
  inherited;
end;

function TExample.test: Boolean;
begin
  Result := False;
end;


var
  example: IBase;

begin
  CoInitialize(nil);

  example := TExample.Create;
  try
    WriteLn(example.test);
  finally
    example := nil;
  end;

  CoUninitialize;
end.

另一件让我感到奇怪的事情是,在基类析构函数中释放FADO对象,因为它们是在派生类中创建的。
作为附注,我更喜欢这种设计:
type
  TBase = class(TInterfacedObject)
    protected
      FQuery: TADOQuery;
      FADOConnection: TADOConnection;
    public
      constructor Create;
      destructor Destroy; override;
  end;

  TExample= class(TBase, IBase)
    public
      function test: Boolean;
      constructor Create;
      destructor Destroy; override;
  end;

{ TBase }

constructor TBase.Create;
begin
  inherited;
  FADOConnection := TADOConnection.Create(nil);
  FQuery := TADOQuery.Create(nil);
end;

destructor TBase.Destroy;
begin
  FQuery.Free;
  FADOConnection.Free;
  inherited;
end;

{ TExample }

constructor TExample.Create;
begin
  inherited;
  . . .
end;

我刚刚意识到你所说的关于构造函数和析构函数的内容。 - Izuel
顺便提一下,在我收到错误之前,你给了我使用CoInitialize(nil)和CoUninitialize的解决方案...谢谢。 - Izuel
抱歉,CoI/CoU 是我在控制台应用程序中的测试的一部分:如果您不使用控制台应用程序,则不需要它们。此外,类的构造函数或析构函数不是插入此类代码的好地方 :D。无论如何,这只是ADO要求的。 - fantaghirocco
我们感到灵感了吗? - Jens Borrisholt
顺便说一下,你对虚构造函数的处理比较薄弱。没有任何证据表明构造函数应该是虚拟的。我认为你应该解决这个问题。 - David Heffernan

-2

这里有几个问题:

首先,正如评论中指出的那样,您需要在析构函数中添加override。但是,TBase的声明实际上是整个问题的根源:

您有一个虚拟的空constructor,它隐藏了来自TObject的构造函数。此外,当您拥有一个TInterfacedObject时,应该使用接口而不是实例。

所以

type TBase = class(TInterfacedObject)
    protected
        FQuery: TADOQuery;
        FADOConnection: TADOConnection;
    public
        constructor Create; virtual; abstract;
        destructor Destroy;
    end;

应该是这样的

  TBase = class(TInterfacedObject)
  protected
    FQuery: TADOQuery;
    FADOConnection: TADOConnection;
  public
    constructor Create; reintroduce; virtual;
    destructor Destroy; override;
  end;

注意 reintroduce

有了这个,以及在析构函数中添加的覆盖,您可以像往常一样释放对象:

procedure TForm1.FormCreate(Sender: TObject);
var
  aux: TExample;
begin
  aux := TExample.Create;
  aux.Free;    
end;

不需要进行强制类型转换。在这里,您创建了一个实例,因此您必须自己释放它。

但正如我之前所说的,当您拥有TInterfacedObject时,应该使用接口来工作,当对象不再被引用时,它会自行释放。

因此,之前的示例应该实际上像这样:

procedure TForm1.FormCreate(Sender: TObject);
var
  aux: iBase;
begin
  aux := TExample.Create;
  //do stuff with aux 
end;

我的完整测试代码如下:

type
  IBase = interface(IInterface)
    function test: Boolean;
  end;

  TBase = class(TInterfacedObject)
  protected
    FQuery: TADOQuery;
    FADOConnection: TADOConnection;
  public
    constructor Create; reintroduce; virtual;
    destructor Destroy; override;
  end;

  TExample = class(TBase, IBase)
  public
    function test: Boolean;
    destructor Destroy; override;
  end;

  { TBase }

constructor TBase.Create;
begin
  inherited;
end;

destructor TBase.Destroy;
begin
  inherited;
end;

{ TExample }

destructor TExample.Destroy;
begin
  test;
  inherited;
end;

function TExample.test: Boolean;
begin
  ShowMessage('');
end;

然后只需调用它:

procedure TForm1.FormCreate(Sender: TObject);
var
  aux: iBase;
begin
  aux := TExample.Create;
end;

2
你绝不能将引用计数对象实例存储在对象引用中,否则会破坏引用计数。请使用aux: IBase代替aux: TExample。此外,引用计数对象不应该手动释放,它们将在最后一个强引用超出作用域时自动释放。 - Dalija Prasnikar
1
@Izuel 请注意我指出的问题。尽管有些人不喜欢我的回答。 - Jens Borrisholt
2
Dalija所说的一点也没错,Jens你应该听从这个建议。 - David Heffernan
@DavidHeffernan 使用 aux:IBase 仍然无法解决代码中的真正问题。 - Jens Borrisholt
1
这就是我们意见不合的地方。如果接口是引用计数的,永远不要直接调用析构函数,也永远不要获取未计数的引用。 - David Heffernan
显示剩余7条评论

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