在Delphi中,为什么传递一个Interface变量有时需要它是一个const参数?

时间:2011-10-03 21:24:44

标签: delphi interface delphi-xe

首先是问题:为什么UnregisterNode()中的const的删除会导致失败,而RegisterNode()则不会。

现在的背景:我正在使用Interfaces在Delphi XE中工作,我遇到了一个让我停顿一下的工件,我得出的结论是我不明白为什么。

不需要显式释放作为接口访问的对象。当最后一个引用超出范围时,它将被销毁。这似乎很简单。我编写了一个测试用例来显示按预期运行的变量和两个失败的变量。六个测试用例仅限于Register和Unregister方法的Node参数的变化。

按表单上的单个按钮可创建容器和三个节点。对它们进行操作以演示程序

该程序创建一些链接到简单容器的简单节点。问题发生在案例#1和#6中。在释放节点时,它会调用容器Unregister()方法。该方法删除指向TList中节点的指针的副本。在两个失败的情况下离开方法时,它会以递归的方式再次调用节点的Destroy()方法,直到发生堆栈溢出。

在有效的四种情况下,Destroy()方法恢复正常,程序将继续并正常退出。

失败#1(案例1)

procedure RegisterNode(Node:INode);
procedure UnregisterNode(Node:INode);

Unregister()方法调用TNode.Destroy()节点似乎会影响INode的引用计数,导致多次调用Destroy(). 为什么会发生这种情况我不明白。当我Register()具有相同样式参数的节点时,不会发生这种情况。

失败#2(案例6)

procedure RegisterNode(const Node:INode);
procedure UnregisterNode(Node:INode);

这里发生了同样的失败模式。如案例5中那样将const添加到参数列表可防止递归调用Destroy()

代码:

unit fMain;
{
   Case 1 - Fails when a node is freed, after unregistering,
             TNode.Destroy is called again
   Case 2 - Passes
   case 3 - Passes
   Case 4 - Passes
   Case 5 - Passes
   Case 6 - Fails the same way as case 1
}
{$Define Case1}
{.$Define Case2}
{.$Define Case3}
{.$Define Case4}
{.$Define Case5}
{.$Define Case6}
interface

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

type

  INode = interface;
  TNode = class;

  IContainer = interface
  ['{E8B2290E-AF97-4ECC-9C4D-DEE7BA6A153C}']
{$ifDef Case1}
    procedure RegisterNode(Node:INode);
    procedure UnregisterNode(Node:INode);
{$endIf}
{$ifDef Case2}
    procedure RegisterNode(Node:TNode);
    procedure UnregisterNode(Node:TNode);
{$endIf}
{$ifDef Case3}
    procedure RegisterNode(const Node:INode);
    procedure UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case4}
    procedure RegisterNode(const Node:TNode);
    procedure UnregisterNode(const Node:TNode);
{$endIf}
{$ifDef Case5}
    procedure RegisterNode(Node:INode);
    procedure UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case6}
    procedure RegisterNode(const Node:INode);
    procedure UnregisterNode(Node:INode);
{$endIf}
  end;
  INode = interface
  ['{37923052-D6D1-4ED5-9AC0-F7FB0076FED8}']
    procedure SetContainer(const Value:IContainer);
    function GetContainer():IContainer;
    procedure ReReg(const AContainer: IContainer);
    procedure UnReg();
    property Container : IContainer
      read GetContainer write SetContainer;
  end;

  TContainer = class(TInterfacedObject, IContainer)
  protected
    NodeList: TList;
  public
    constructor Create(); virtual;
    destructor Destroy(); override;
{$ifDef Case1}
    procedure RegisterNode(Node:INode); virtual;
    procedure UnregisterNode(Node:INode); virtual;
{$endIf}
{$ifDef Case2}
    procedure RegisterNode(Node:TNode); virtual;
    procedure UnregisterNode(Node:TNode); virtual;
{$endIf}
{$ifDef Case3}
    procedure RegisterNode(const Node:INode); virtual;
    procedure UnregisterNode(const Node:INode); virtual;
{$endIf}
{$ifDef Case4}
    procedure RegisterNode(const Node:TNode); virtual;
    procedure UnregisterNode(const Node:TNode); virtual;
{$endIf}
{$ifDef Case5}
    procedure RegisterNode(Node:INode); virtual;
    procedure UnregisterNode(const Node:INode); virtual;
{$endIf}
{$ifDef Case6}
    procedure RegisterNode(const Node:INode); virtual;
    procedure UnregisterNode(Node:INode); virtual;
{$endIf}
  end;

  TNode = class(TInterfacedObject, INode)
  protected
    FContainer : IContainer;
  public
    constructor Create(const AContainer: IContainer); virtual;
    destructor Destroy(); override;
    procedure SetContainer(const Value:IContainer); virtual;
    function GetContainer():IContainer; virtual;
    procedure ReReg(const AContainer: IContainer); virtual;
    procedure UnReg(); virtual;
    property Container : IContainer
      read GetContainer write SetContainer;
  end;

  TForm1 = class(TForm)
    btnMakeStuff: TButton;
    procedure btnMakeStuffClick(Sender: TObject);
  private
    { Private declarations }
    MyContainer : IContainer;
    MyNode1,
    MyNode2,
    MyNode3     : INode;

  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation
{$R *.dfm}

{ TContainer }

constructor TContainer.Create();
begin
  inherited;
  NodeList := TList.Create();
end;
destructor TContainer.Destroy();
var
  i : integer;
begin
  for i := 0 to Pred(NodeList.Count) do
    INode(NodeList.Items[i]).Container := nil;  //Prevent future Node from contacting container
  NodeList.Free();
  inherited;
end;

{$ifDef Case1}
procedure TContainer.RegisterNode(Node:INode);
{$endIf}
{$ifDef Case2}
procedure TContainer.RegisterNode(Node:TNode);
{$endIf}
{$ifDef Case3}
procedure TContainer.RegisterNode(const Node:INode);
{$endIf}
{$ifDef Case4}
procedure TContainer.RegisterNode(const Node:TNode);
{$endIf}
{$ifDef Case5}
procedure TContainer.RegisterNode(Node:INode);
{$endIf}
{$ifDef Case6}
procedure TContainer.RegisterNode(const Node:INode);
{$endIf}

begin
  NodeList.Add(pointer(Node));
end;

{$ifDef Case1}
procedure TContainer.UnregisterNode(Node:INode);
{$endIf}
{$ifDef Case2}
procedure TContainer.UnregisterNode(Node:TNode);
{$endIf}
{$ifDef Case3}
procedure TContainer.UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case4}
procedure TContainer.UnregisterNode(const Node:TNode);
{$endIf}
{$ifDef Case5}
procedure TContainer.UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case6}
procedure TContainer.UnregisterNode(Node:INode);
{$endIf}
var
  i : integer;
begin
  i := NodeList.IndexOf(pointer(Node));
  if i >= 0 then
    NodeList.Delete(i);
end;

{ INode }

constructor TNode.Create(const AContainer: IContainer);
begin
  ReReg(AContainer);
end;

destructor TNode.Destroy();
begin   {When failing, after unregistering, it returns here !!!!}
  if Assigned(FContainer) then begin
    FContainer.UnregisterNode(self);
  end;
  inherited;
end;

function TNode.GetContainer(): IContainer;
begin
  Result := FContainer;
end;

procedure TNode.ReReg(const AContainer: IContainer);
begin
  if Assigned(AContainer) then
    AContainer.RegisterNode(Self);
  FContainer := AContainer;
end;

procedure TNode.SetContainer(const Value: IContainer);
begin
  if Assigned(FContainer) then
    FContainer.UnregisterNode(self);
  FContainer := Value;
  FContainer.RegisterNode(self);
end;

procedure TNode.UnReg();
begin
  if Assigned(FContainer) then
    FContainer.UnregisterNode(self);
  FContainer := nil;
end;

 { TForm1 }

procedure TForm1.btnMakeStuffClick(Sender: TObject);
begin
  MyContainer := TContainer.Create();
  MyNode1 := TNode.Create(MyContainer);
  MyNode2 := TNode.Create(MyContainer);
  MyNode3 := TNode.Create(MyContainer);

  MyNode2.UnReg();  //Breakpoint here
  MyNode2.ReReg(MyContainer);  //Breakpoint here
  MyNode3 := nil;   //Case 1 & 6 cause a stackoverflow
  MyNode2 := nil;

end;

end.

3 个答案:

答案 0 :(得分:27)

参数上的 const 指令表示过程/函数不会修改该参数中提供的值。如果过程或函数希望操作任何 const 参数,则首先必须将该值复制到局部变量。

这允许编译器对这些参数执行一些优化,特别是在引用类型(如字符串和接口等)方面。

使用特定接口,因为参数声明为 const ,所以传递的接口引用的值不可能在参数的“生存期”内被修改(因为编译器将拒绝任何代码)尝试修改值),因此编译器能够消除对 AddRef() Release()的调用,这些调用将以其他方式生成为prolog和epilog in那个程序。

但请注意,如果将引用分配给其他变量,则在过程体内,引用计数仍可能会更改。 const 优化只是消除了对一个 AddRef / Release对的可能需求。

const 和非 const 参数之间的引用计数行为的差异显然会产生一些副作用或与代码中其他复杂性的其他交互,但现在了解 const 的效果您可能能够确定在其他地方出错的方式/位置。

事实上,我可以告诉你哪里出错了。 :)

除非你非常非常确定你在做什么,否则你不应该直接将接口引用转换为/从任何其他类型(接口或指针或其他类型)。您应始终使用作为 QueryInterface()从一种接口类型转换为另一种接口类型:

  otherRef := fooRef as IOther;

并且您应始终使用 IUnknown (或 IInterface )作为“无类型”接口引用,而不是指针。这可以确保您的引用都是所有属性。 (有时你想要一个不计数的引用,因此会使用类型转换指针引用,但这是非常高级voodoo )。

在您的示例代码中,转换为指针类型以将其保存在 TList 中是颠覆引用计数机制并结合 const / non const 参数会导致您看到的副作用。

要维护列表中接口的正确计数引用,请使用界面友好列表类,例如 TList<接口类型> TInterfaceList (如果您不喜欢)泛型,没有它们可用,或者可能需要与没有它的人分享你的代码。

<强>脚注:

另外要注意:当接口引用计数降至零时,对象的销毁不一定像您想象的那样自动。

它是特定接口对象类的实现细节。如果你在TInterfacedObject上检查_Release()实现的源代码,你会看到这是如何实现的。

简单地说,对象本身负责在自己的引用计数达到零时销毁自己。实际上,该对象甚至负责首先实现引用计数!因此,完全有可能(有时候还需要)一个专门的类来覆盖或替换这种行为,在这种情况下它如何响应零引用计数(或者实际上它是否甚至困扰维持引用计数)完全取决于它自己的需求。

话虽如此,绝大多数实现接口的对象几乎肯定会使用这种形式的自动销毁,但不应该简单地假设它。

应该可以安全地假设,如果给你一个对象的接口引用,你通常不会关心该对象最终将如何被销毁。但这并不等于说你可以假设当接口引用计数达到零时它将被销毁。

我之所以提到这一点是因为了解所有这些明显的“编译器魔术”是如何工作的,对于理解在这种情况下遇到的问题至关重要。

答案 1 :(得分:14)

接口的引用计数

您的原始问题以及对此答案的评论中的跟进都取决于Delphi的界面引用计数机制。

编译器发出代码以安排对接口的所有引用进行计数。无论何时采用新的参考,计数都会增加。每当释放引用(设置为nil,超出范围等)时,计数就会减少。当计数达到零时,界面将被释放,在您的情况下,这就是对象上的Free调用。

您的问题是,您通过将TList转换为Pointer并将其引入Pointer并将其返回到作为参与计数来作弊。在某处,引用被错误计算。我确定你的代码的行为(即堆栈溢出)可以解释,但我不愿意尝试这样做,因为代码使用了这些明显不正确的结构。

简单地说,您永远不应该将接口强制转换为非托管类型,例如TList<INode>。每当您这样做时,您还需要控制丢失的引用计数代码。我可以向你保证,这是你不想承担的事情!

您应该使用正确的类型安全容器,如UnReg甚至是动态数组,然后才能正确处理引用计数。对代码进行此更改可以解决您在问题中描述的问题。

循环参考

然而,仍然存在一个大问题,正如您自己发现并在评论中详细说明的那样。

按照引用计数规则,您将面临循环引用的问题。在这种情况下,节点保存对容器的引用,该容器又保持对节点的引用。像这样的循环引用不能被标准引用计数机制打破,你必须自己打破它们。一旦你打破构成循环引用的两个单独引用之一,框架就可以完成其余的工作。

使用您当前的设计,您必须在您创建的每个INode上显式调用MyContainer来中断循环引用。

代码的另一个问题是,您使用表单的数据字段来保留MyNodeMyContainer等。因为您从未将nil设置为{{1然后两次执行你的事件处理程序将导致泄漏。

对您的代码进行了以下更改,以证明它将在不泄漏的情况下运行:

TContainer = class(TInterfacedObject, IContainer)
protected
  NodeList: TList<INode>;//switch to type-safe list

...

procedure TContainer.RegisterNode(Node:INode);
begin
  //must ensure we don't add the node twice
  if NodeList.IndexOf(Node) = -1 then
    NodeList.Add(Node);
end;

...

procedure TForm1.btnMakeStuffClick(Sender: TObject);
//make the interfaces local variables although in production
//code they would likely be fields and construction would happen
//in the constructor of the owning object
var
  MyContainer: IContainer;
  MyNode1, MyNode2, MyNode3: INode;
begin
  MyContainer := TContainer.Create;
  MyNode1 := TNode.Create(MyContainer);
  MyNode2 := TNode.Create(MyContainer);
  MyNode3 := TNode.Create(MyContainer);

  MyNode1.UnReg;
  MyNode1.ReReg(MyContainer);
  MyNode2.UnReg;
  MyNode3.UnReg;
  MyNode2.ReReg(MyContainer);
  MyNode1.UnReg;
  MyNode2.UnReg;
end;

通过这些更改,代码可以在没有内存泄漏的情况下运行 - 在.dpr文件的开头设置ReportMemoryLeaksOnShutdown := True以进行检查。


要在每个节点上调用UnReg,这将是一种绑定,所以我建议您只需向IContainer添加一个方法即可。一旦您安排容器能够删除其引用,那么您将拥有一个更易于管理的系统。

您将无法让引用计数为您完成所有工作。您需要明确调用IContainer.UnRegAllItems

您可以像这样实现这个新方法:

procedure TContainer.UnRegAllItems;
begin
  while NodeList.Count>0 do
    NodeList[0].UnReg;
end;

引用计数错误

尽管Delphi引用计数机制在一般情况下得到了很好的实现,但据我所知,还有一个长期存在且非常着名的bug。

procedure Foo(const I: IInterface);
begin
  I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);

以这种方式调用Foo时,不会生成任何代码来添加对接口的引用。因此,界面在创建后立即释放,Foo作用于无效的界面。

由于Foo接收参数constFoo不会引用该接口。该错误发生在Foo调用的codegen中,错误地没有引用该接口。

我解决这个问题的首选方法是:

var
  I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);

这是成功的,因为我们明确地引用了一个。

请注意,我已经解释了这一点以供将来参考 - 您当前的代码不会违反此问题。

答案 2 :(得分:4)

如果我理解你的话,你就是从 TNode.Destroy 中调用 UnregisterNode()

destructor TNode.Destroy;
begin
  ...
  UnregisterNode(Self);
  ...
end;

INode 在其生命周期结束时,即当其引用计数为0时,您可能会这样做。

如果 UnregisterNode 执行 const 参数,将在上完成_ AddRef 自我,将引用计数恢复为1,在 UnregisterNode 结束时,将完成_ 发布,这会将引用计数恢复为0 ,这意味着再次调用 Destroy ,并且存在间接递归循环,导致堆栈溢出。

如果 UnregisterNode 采用 const 参数,则不会执行_ AddRef ,也不会执行_ 发布,所以你不会进入递归循环。

如果您确保 RegisterNode 正确保留节点,则不会发生此类问题,即增加其引用计数并保持该方式,即将其存储在类型安全列表中,例如, &的TList LT; INODE&GT;