不带引用计数的接口

6

阅读了 StackOverflow 上关于使用自动引用计数的接口存在缺点的帖子之后,我开始尝试手动引用计数每个接口实例。

但是尝试了一个下午后,我放弃了!

为什么我在调用 FreeAndNil(p) 时会出现访问冲突?

以下是我的简单单元的完整列表。

unit fMainForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm4 = class(TForm)
    btn1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure btn1Click(Sender: TObject);
  end;

type
  IPersona = interface(IInterface)
  ['{44483AA7-2A22-41E6-BA98-F3380184ACD7}']
    function GetNome: string;
    procedure SetNome(const Value: string);
    property Nome: string read GetNome write SetNome;
  end;

type
  TPersona = class(TObject, IPersona)
  strict private
    FNome: string;
    function GetNome: string;
    procedure SetNome(const Value: string);
  protected
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  public
    constructor Create(const ANome: string);
    destructor Destroy; override;
  end;

var
  Form4: TForm4;

implementation

{$R *.dfm}

procedure TForm4.FormCreate(Sender: TObject);
begin
  ReportMemoryLeaksOnShutdown := True;
end;

procedure TForm4.btn1Click(Sender: TObject);
var
  p: IPersona;
begin
  p := TPersona.Create('Fabio');
  try
    ShowMessage(p.Nome);
  finally
    FreeAndNil(p);
  end;
end;

constructor TPersona.Create(const ANome: string);
begin
  inherited Create;
  FNome := ANome;
end;

destructor TPersona.Destroy;
begin
  inherited Destroy;
end;

function TPersona._AddRef: Integer;
begin
  Result := -1
end;

function TPersona._Release: Integer;
begin
  Result := -1
end;

function TPersona.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := S_OK
  else
    Result := E_NOINTERFACE;
end;

function TPersona.GetNome: string;
begin
  Result := FNome;
end;

procedure TPersona.SetNome(const Value: string);
begin
  FNome := Value;
end;

end.
2个回答

11

访问冲突发生是因为FreeAndNil收到一个未定义类型的var参数,而该参数预期应该是一个对象引用。你正在传递接口引用,这不符合要求。不幸的是,只有在运行时才会发现这一点。这是我看来反对使用FreeAndNil的最有力的观点。

通过接口引用计数机制禁用了生命周期管理。为了销毁对象,需要调用其析构函数。为此,必须能够访问析构函数。但是你的接口并没有公开析构函数(也不应该公开)。因此,我们可以推断出,为了销毁对象,需要拥有一个对象引用。

以下是一些选项:

var
  obj: TPersona;
  intf: IPersona;
....
obj := TPersona.Create('Fabio');
try
  intf := obj;
  //do stuff with intf
finally
  obj.Free;
  // or FreeAndNil(obj) if you prefer
end;

或者你可以这样做

var
  intf: IPersona;
....
intf := TPersona.Create('Fabio');
try
  //do stuff with intf
finally
  (intf as TObject).Free;
end;

+1 <duh> 我根本不知道你可以将接口直接转换为 TObject。 (我还没有测试过,但从你这里得到的信息,我认为它是正确的) - Lieven Keersmaekers
2
@Lieven 只有在一些较新的 Delphi 版本中才支持该功能。要么是 2009 年,要么是 2010 年。我记不清具体是哪个版本了。 - David Heffernan
David谢谢!你的解决方案(两个)非常好用(就像你在StackOverflow上发布的每一个答案一样;-))太棒了!!! - Fabio Vitale
作为好奇心的问题:我是否总是需要在接口声明中放置GUID?我尝试在我的代码中删除GUID,一切似乎都正常工作。 - Fabio Vitale
3
我总是把它们加进去,因为如果没有它们,你就不能使用as运算符、Supports等将一个接口转换为另一个接口。 - David Heffernan

0

你不能使用FreeAndNil()释放接口引用,只能使用对象引用。如果你使接口的引用计数启用,那么你可以直接将nil赋值给接口引用(或者让它超出范围)以正确释放对象,例如:

type
  TPersona = class(TInterfacedObject, IPersona)
  strict private
    FNome: string;
    function GetNome: string;
    procedure SetNome(const Value: string);
  public
    constructor Create(const ANome: string);
    destructor Destroy; override;
  end;

procedure TForm4.btn1Click(Sender: TObject);
var
  p: IPersona;
begin
  p := TPersona.Create('Fabio');
  try
    ShowMessage(p.Nome);
  finally
    p := nil;
  end;
end;

但是,由于您已禁用了接口上的引用计数,因此您需要返回使用代码中的普通对象引用变量,例如:

procedure TForm4.btn1Click(Sender: TObject);
var
  p: TPersona;
  intf: IPersona;
begin
  p := TPersona.Create('Fabio');
  try
    if Supports(p, IPersona, intf) then
      ShowMessage(intf.Nome);
  finally
    FreeAndNil(p);
  end;
end;

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