在Delphi中,为什么传递接口变量有时要求它是常量参数?

在Delphi中,为什么传递接口变量有时要求它是常量参数?,delphi,interface,delphi-xe,Delphi,Interface,Delphi Xe,首先是一个问题:为什么在UnregisterNode()中删除常量会导致失败,而在RegisterNode()中则不会 现在是背景:我在Delphi XE中使用接口,我遇到了一个让我停顿的工件,我得出了一个结论,我真的不明白为什么 作为接口访问的对象不需要显式释放。当最后一个引用超出范围时,它将被销毁。这似乎很简单。我已经编写了一个测试用例来显示按预期运行的变体和两个失败的变体。这六个测试用例仅限于Register和Unregister方法的节点参数的变化 按下表单上的lone按钮创建容器和三个

首先是一个问题:为什么在
UnregisterNode()
中删除常量会导致失败,而在
RegisterNode()中则不会

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

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

按下表单上的lone按钮创建容器和三个节点。对其进行操作,以演示程序

程序将创建一些链接到简单容器的简单节点。问题发生在案例1和案例6中。释放节点时,它调用containers
Unregister()
方法。该方法删除指向TList中节点的指针的副本。在这两种失败的情况下,当保留该方法时,它会递归地调用节点的
Destroy()
方法,重新启动进程,直到发生堆栈溢出

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

故障#1(案例1)

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

失败#2(案例6)

同样的故障模式也发生在这里。将常量添加到参数列表(如案例5所示)可防止对
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.
接口的引用计数 您最初的问题以及对该答案的评论中的后续内容都取决于Delphi的接口引用计数机制

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

您的问题是,您通过将接口引用放入
TList
并来回转换到
指针来欺骗引用计数。在这条路上的某个地方,参考资料被算错了。我相信您的代码的行为(即堆栈溢出)是可以解释的,但我不愿意尝试这样做,因为代码使用了明显不正确的构造

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

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

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

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

在当前的设计中,必须通过在创建的每个
INode
上显式调用
unr
来打破循环引用

当前代码的另一个问题是,您正在使用表单的数据字段来保存
MyContainer
MyNode
等。因为您从未将
MyContainer
设置为
nil
,所以事件处理程序的两次执行将导致泄漏

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

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;
引用计数错误 尽管Delphi引用计数机制在总体上得到了很好的实现,但据我所知,有一个长期存在且非常著名的bug

procedure Foo(const I: IInterface);
begin
  I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
以这种方式调用
Foo
时,不会生成代码来添加对接口的引用。因此,一旦创建了接口,
Foo
就会对无效的接口进行操作

因为
Foo
接收参数为
const
Foo
不引用接口。错误在调用
Foo
的codegen中,它错误地没有引用接口

我比较喜欢的解决这个问题的方法是:

procedure TContainer.UnRegAllItems;
begin
  while NodeList.Count>0 do
    NodeList[0].UnReg;
end;
var
  I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
这成功了,因为我们显式地获取了引用


请注意,我已对此进行了解释,以供将来参考–您当前的代码不会与此问题相冲突。

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

这允许编译器对这些参数执行一些优化,特别是i
procedure Foo(const I: IInterface);
begin
  I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
var
  I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
  otherRef := fooRef as IOther;
destructor TNode.Destroy;
begin
  ...
  UnregisterNode(Self);
  ...
end;