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

Ric*_*ler 17 delphi interface delphi-xe

首先是问题:为什么删除const UnregisterNode()导致失败,而不是导致失败RegisterNode().

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

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

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

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

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

失败#1(案例1)

procedure RegisterNode(Node:INode);
procedure UnregisterNode(Node:INode);
Run Code Online (Sandbox Code Playgroud)

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

失败#2(案例6)

procedure RegisterNode(const Node:INode);
procedure UnregisterNode(Node:INode);
Run Code Online (Sandbox Code Playgroud)

这里发生了同样的失败模式.如案例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.
Run Code Online (Sandbox Code Playgroud)

Del*_*ics 29

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

这允许编译器对这些参数执行一些优化,特别是在诸如字符串和接口等的引用类型的区域中.

特别是使用接口,由于参数被声明为const,因此传递的接口引用的值不可能在参数的"生命周期"期间被修改(因为编译器将拒绝任何尝试修改该值的代码),因此编译器能够消除对AddRef()Release()的调用,否则将在该过程中生成prolog和epilog.

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

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

事实上,我可以告诉你哪里出了问题.:)

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

  otherRef := fooRef as IOther;
Run Code Online (Sandbox Code Playgroud)

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

在您的示例代码中,转换为/来自指针类型以在TList中维护它们是颠覆引用计数机制,并结合const/non const参数的变化导致您看到的副作用.

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

脚注:

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

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

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

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

什么应该是安全的假设是,如果你将得到一个接口引用一个对象,你通常不会用怎么说对象最终被破坏有关.但这与说你可以假设当接口引用计数达到零时它将被销毁是不一样的.

我之所以提到这一点是因为了解所有这些明显的"编译器魔术"是如何工作的,对于理解诸如此类案例中遇到的问题至关重要.

  • @David,不 - 那个"事实上......"并不针对任何人或任何事情.考虑到在另一个答案的评论中提出了铸造问题,我打算将答案保留原样.在一个"意识流"的时刻,我决定在答案中突出问题的这个方面,为了未来任何读者的问题/答案(不是每个人都阅读评论).就这些.:) (2认同)

Dav*_*nan 14

接口的引用计数

您的原始问题以及对此答案的评论中的后续操作都取决于Delphi的界面引用计数机制.

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

您的问题是,您通过将接口引用放入和转出TList逐行转换来欺骗引用计数Pointer.在某处,引用被错误计算.我确定你的代码的行为(即堆栈溢出)可以解释,但我不愿意尝试这样做,因为代码使用这种明显不正确的结构.

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

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

循环参考

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

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

使用当前的设计,您必须通过显式调用您创建的UnReg每个参数来打破循环引用INode.

的另一个问题,因为它代表的代码是您使用的表单的数据字段来保存MyContainer,MyNode等等.因为你永远不设置MyContainernil那么你的两个事件处理程序的执行将导致泄漏.

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

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;
Run Code Online (Sandbox Code Playgroud)

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


如果必须调用UnReg每个节点,这将是一个绑定,所以我建议您只需添加一个方法来IContainer实现这一点.一旦您安排容器能够删除其引用,那么您将拥有一个更易于管理的系统.

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

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

procedure TContainer.UnRegAllItems;
begin
  while NodeList.Count>0 do
    NodeList[0].UnReg;
end;
Run Code Online (Sandbox Code Playgroud)

引用计数错误

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

procedure Foo(const I: IInterface);
begin
  I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
Run Code Online (Sandbox Code Playgroud)

Foo以这种方式呼叫不产生代码的引用添加到所述接口.因此,界面在创建后立即释放,并Foo作用于无效接口.

因为Foo接收参数const,Foo不参考接口.错误是在codegen中进行的调用Foo,错误地没有引用该接口.

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

var
  I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
Run Code Online (Sandbox Code Playgroud)

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

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