在 Delphi 中,为什么传递 Interface 变量有时需要它是 const 参数?
首先是问题:为什么在 UnregisterNode()
中删除 const 会导致失败,而在 RegisterNode()
中则不会。
现在是背景:我正在 Delphi XE 中使用 Interfaces 工作,我遇到了一个工件,它让我停顿了一下,我得出的结论是我真的不明白为什么。
作为接口访问的对象不需要显式释放。当最后一个引用超出范围时,它就会被销毁。这看起来很简单。我编写了一个测试用例来显示按预期运行的变体和失败的两个变体。这六个测试用例仅限于 Register 和 Unregister 方法的 Node 参数的变化。
按表单上的单独按钮将创建容器和三个节点。对它们执行操作以演示该过程
该程序创建一些链接到简单容器的简单节点。问题发生在案例 #1 和案例 #6 中。当节点被释放时,它会调用容器的Unregister()
方法。该方法删除指向 TList 中节点的指针的副本。当该方法处于两种失败情况时,它会调用节点的 Destroy()
方法,递归地重新启动该过程,直到发生堆栈溢出。
在四种有效的情况下,Destroy()
方法将正常恢复,程序将正常继续和退出。
失败 #1(情况 1)
procedure RegisterNode(Node:INode);
procedure UnregisterNode(Node:INode);
从 TNode.Destroy()
方法调用 Unregister()
节点似乎会影响 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.
First the question: Why does the removal of const in UnregisterNode()
cause failure, but not in RegisterNode()
.
Now the background: I’m working in Delphi XE with Interfaces and I ran into an artifact that has given me some pause and I’ve come to the conclusion that I don’t really understand why.
An object that is accessed as an interface does not need to be explicitly freed. When the last reference goes out of scope, it is destroyed. That seems simple enough. I have written a test case to show variations that run as expected and two that fail. The six test cases are limited to variations on the Node parameter of the Register and Unregister methods.
Pressing the lone button on the form creates the container and three nodes. Operations are preformed on them to demonstrate the procedure
The program creates some simple nodes that link to a simple container. The problem happened in cases #1 and #6. When the node is being freed, it calls the containers Unregister()
method. The method deletes a copy of the pointer to the node in a TList. When leaving the method in the two failed cases it calls the node’s Destroy()
method recursively starting the process over again until a stack overflow occurs.
In the four cases that work the Destroy()
method is resumed as normal and the program will proceed and exit normally.
Failure #1 (Case 1)
procedure RegisterNode(Node:INode);
procedure UnregisterNode(Node:INode);
Calling the Unregister()
node from the TNode.Destroy()
method seems affect the reference count of the INode causing multiple calls to Destroy().
Why this happens I don’t understand. It does not happen when I Register()
the node with the same style of parameters.
Failure #2 (Case 6)
procedure RegisterNode(const Node:INode);
procedure UnregisterNode(Node:INode);
The same failure pattern happens here. Adding const to the parameter list as in Case 5 prevents the recursive calls to Destroy()
.
The code:
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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
参数上的 const 指令指示过程/函数不会修改该参数中提供的值。如果过程或函数希望操作任何const参数,它首先必须将该值复制到局部变量。
这允许编译器对这些参数执行一些优化,特别是在引用类型(例如字符串和接口等)领域。
特别是对于接口,由于参数被声明为 const,因此不可能将其值设置为 const。在参数的“生命周期”内要修改的接口引用(因为编译器将拒绝任何尝试修改该值的代码),因此编译器能够消除对 AddRef() 和Release() 否则将在该过程中生成为序言和结尾。
但请注意,在过程体内,如果将引用分配给其他变量,则引用计数仍然可能会发生变化。 const 优化只是消除了对一个 AddRef/Release 对的可能需求。
const 和非 const 参数之间引用计数行为的差异显然会产生一些副作用或与代码中其他复杂性的其他交互,但现在了解 const 的影响strong>const你也许能够确定你在其他地方如何/哪里出错了。
事实上,我可以告诉你你错在哪里。 :)
你永远不应该直接将接口引用转换为任何其他类型(接口或指针或其他类型),除非你非常非常确定你在做什么。您应该始终使用 as 或 QueryInterface() 从一种接口类型转换为另一种接口类型:
并且您应该始终使用 IUnknown (或 IInterface)作为“无类型”接口引用,而不是指针。这可确保您的推荐人的所有财产均已入账。 (有时您需要一个不可计数的引用,因此会使用类型转换指针引用,但这是非常高级巫毒)。
在您的示例代码中,与 pointer 类型之间的转换以将它们维护在 TList 中,这破坏了引用计数机制,并与 const< 中的变体结合在一起/strong>/non-const 参数会导致您看到的副作用。
要维护对列表中接口的正确计数的引用,请使用接口友好的列表类,例如 TList 或 TInterfaceList(如果您不喜欢泛型,请不要使用您没有可用的代码,或者可能需要与没有可用代码的人共享您的代码)。
脚注:
还要注意:当接口引用计数降至零时对象的销毁不一定像您想象的那么自动。
这是特定的实现细节接口对象类。如果您检查 TInterfacedObject 上 _Release() 实现的源代码,您将看到这是如何实现的。
简单地说,当对象本身的引用计数达到零时,它自己负责销毁自己。事实上,该对象甚至首先负责实现引用计数!因此,专门的类完全有可能(有时是可取的)覆盖或替换此行为,在这种情况下,它如何响应零引用计数(或者实际上是否需要维护引用计数本身)完全取决于它自己的需要。
话虽如此,绝大多数实现接口的对象几乎肯定会使用这种形式的自动销毁,但不应简单地假设这一点。
可以安全地假设的是,如果给您一个对象的接口引用,您通常不会关心该对象最终将如何被销毁。但这并不等于您可以假设当接口引用计数达到零时它将被销毁。
我之所以提到这一点,是因为了解所有这些明显的“编译器魔法”的工作原理对于理解诸如本例中遇到的问题至关重要。
The const directive on a parameter indicates that the procedure/function will not modify the value supplied in that parameter. If the procedure or function wishes to manipulate any const parameter it will first have to copy that value to a local variable.
This allows the compiler to perform some optimisations on such parameters, particularly in the area of reference types such as strings and interfaces etc.
With interfaces specifically, since the parameter is declared const it is impossible for the value of the interface reference passed to be modified during the "lifetime" of the parameter (since the compiler will reject any code that tries to modify the value), thus the compiler is able to eliminate the calls to AddRef() and Release() that would other wise be generated as prolog and epilog in that procedure.
Note however that within the body of the procedure if the reference is assigned to other variables then the reference count could still change. The const optimisation simply eliminates the possible need for one AddRef/Release pair.
This difference in reference counting behaviour between const and non-const parameters is obviously having some side effect or other interaction with the other complexities in your code but now understanding the effect of const you might be able to determine how/where you may have gone wrong elsewhere.
In fact, I can tell you where you have gone wrong. :)
You should never directly cast an interface reference to/from any other type (interface or pointer or otherwise) unless you are very VERY sure of what you are doing. You should always use as or QueryInterface() to cast from one interface type to another:
And you should always use IUnknown (or IInterface) as an 'untyped' interface reference, not a pointer. This ensures that your references are all property accounted for. (there are times when you want an uncounted reference and thus would use a type-cast pointer reference, but that is very advanced voodoo).
In your sample code, the casting to/from pointer type to maintain them in a TList is subverting the reference counting mechanism and in conjunction with the variations in const/non-const parameters is leading to the side effects you are seeing.
To maintain properly counted references to interfaces in a list, use an interface friendly list class such as TList<Interface Type> or TInterfaceList (if you don't like generics, don't have them available to you, or may need to share your code with someone that doesn't).
Footnote:
Also beware: The destruction of an object when the interface reference count drops to zero is not necessarily quite as automatic as you think.
It is an implementation detail of the particular interfaced object class. If you inspect the source of the _Release() implementation on TInterfacedObject you will see how this is possible.
Simply put, the object itself is responsible for destroying itself when it's own reference count reaches zero. In fact, the object is even responsible for implementing the reference count in the first place! It is perfectly possible therefore (and sometimes desirable) for a specialised class to override or replace this behaviour in which case how it responds to a zero reference count (or indeed whether it even bothers to maintain a reference count as such) is entirely up to its own needs.
Having said that, the overwhelming majority of objects that implement interfaces will almost certainly use this form of auto-destruction, but it should not simply be assumed.
What should be safe to assume is that if you are given an interface reference to an object, you would not normally be concerned with how that object will ultimately be destroyed. But that is not the same as saying you can assume it will be destroyed when the interface reference count reaches zero.
I mention this because being aware of how all this apparent "compiler magic" works can be critical to understanding problems such as those you have run into in this case.
接口的引用计数
您原来的问题以及此答案的评论中的后续内容都取决于 Delphi 的接口引用计数机制。
编译器发出代码来安排对接口的所有引用都进行计数。每当您获取新的参考时,计数就会增加。每当引用被释放(设置为 nil、超出范围等)时,计数就会减少。当计数达到零时,接口被释放,在您的情况下,这就是对对象调用
Free
的原因。您的问题是,您通过转换为
Pointer
并返回,将接口引用放入和取出TList
,从而欺骗了引用计数。一路上的某个地方,参考文献被错误地统计了。我确信您的代码的行为(即堆栈溢出)可以得到解释,但我不愿意尝试这样做,因为代码使用了如此明显不正确的构造。简而言之,您永远不应该将接口强制转换为非托管类型,例如
Pointer
。每当您这样做时,您还需要控制丢失的引用计数代码。我可以向你保证,这是你不想承担的事情!您应该使用适当的类型安全容器,例如
TList
甚至动态数组,然后引用计数将得到正确处理。对代码进行此更改可以解决您在问题中描述的问题。循环引用
然而,正如您自己发现的并在评论中详细说明的那样,仍然存在一个大问题。
一旦遵循引用计数规则,您就会面临循环引用的问题。在这种情况下,节点保存对容器的引用,而容器又保存对节点的引用。像这样的循环引用不能被标准引用计数机制打破,你必须自己打破它们。一旦破坏了构成循环引用的两个单独引用之一,框架就可以完成剩下的工作。
根据您当前的设计,您必须通过在您创建的每个
INode
上显式调用UnReg
来打破循环引用。代码的另一个问题是,您正在使用表单的数据字段来保存
MyContainer
、MyNode
等。因为您从未设置MyContainer 为
nil
那么事件处理程序的两次执行将导致泄漏。对代码进行了以下更改,以证明它将运行而不会泄漏:
通过这些更改,代码运行时不会出现内存泄漏 - 在 .dpr 文件的开头设置
ReportMemoryLeaksOnShutdown := True
进行检查。必须在每个节点上调用
UnReg
会是一种绑定,因此我建议您只需向IContainer
添加一个方法来执行此操作。一旦您安排容器能够删除其引用,那么您将拥有一个更易于管理的系统。您将无法让引用计数为您完成所有工作。您需要显式调用
IContainer.UnRegAllItems
。您可以像这样实现这个新方法:
引用计数错误
虽然 Delphi 引用计数机制总体上实现得很好,但据我所知,有一个长期存在且非常著名的错误。
当以这种方式调用
Foo
时,不会生成任何代码来添加对接口的引用。因此,该接口在创建后立即被释放,并且Foo
作用于无效接口。由于
Foo
接收的参数为const
,因此Foo
不会引用接口。该错误存在于调用Foo
的代码生成器中,它错误地没有引用接口。我解决这个特定问题的首选方法是这样的:
这成功了,因为我们明确地采用了引用。
请注意,我已经对此进行了解释以供将来参考 - 您当前的代码不会遇到此问题。
Reference counting for interfaces
Your original question and the follow up in the comments to this answer all hinge on Delphi's interface reference counting mechanism.
The compiler emits code to arrange that all references to an interface are counted. Whenever you take a new reference, the count is increased. Whenever a reference is released (set to
nil
, goes out of scope etc.) the count is decreased. When the count reaches zero, the interface is released and in your case this is what callsFree
on your objects.Your problem is that you are cheating the reference counting by putting interface references into and out of the
TList
by casting toPointer
and back. Somewhere along the way the references are miscounted. I'm sure your code's behaviour (i.e. the stack overflows) could be explained but I am disinclined to attempt to do so since the code uses such obviously incorrect constructs.Simply put you should never cast an interface to an unmanaged type like
Pointer
. Whenever you do so you also need to take control of the missing reference counting code. I can assure you this is something you do not want to take on!You should use a proper type-safe container like
TList<INode>
or even a dynamic array and then the reference counting will be handled correctly. Making this change to your code solves the problems you describe in the question.Circular references
However, there still remains one big problem, as you have discovered for yourself and detailed in the comments.
Once you follow the reference counting rules, you are faced with the problem of circular references. In this case a node holds a reference to the container which in turn holds a reference to the node. Circular references like this cannot be broken by the standard reference counting mechanism and you have to break them yourself. Once you break one of the two individual references that make up a circular reference, the framework can do the rest.
With your current design you must break the circular references by explicitly calling
UnReg
on everyINode
that you create.The other problem with the code as it stands is that you are using data fields of the form to hold
MyContainer
,MyNode
etc. Because you never setMyContainer
tonil
then two executions of your event handler will result in a leak.In made the following changes to your code to prove that it will run without leaking:
With these changes the code runs without memory leaks – set
ReportMemoryLeaksOnShutdown := True
at the start of the .dpr file to check.It is going to be something of a bind to have to call
UnReg
on every node so I suggest that you simply add a method toIContainer
to do that. Once you arrange that the container is capable of dropping its references then you will have a much more manageable system.You will not be able to let reference counting do all the work for you. You will need to call
IContainer.UnRegAllItems
explicitly.You can implement this new method like this:
Reference counting bugs
Although the Delphi reference counting mechanism is very well implemented in general, there is, to my knowledge, one long-standing and very well-known bug.
When
Foo
called in this way no code is generated to add a reference to the interface. The interface is thus released as soon as it is created andFoo
acts on an invalid interface.Because
Foo
receives the parameter asconst
,Foo
does not take a reference to the interface. The bug is in the codegen for the call toFoo
which mistakenly does not take a reference to the interface.My preferred way to work around this particular problem is like this:
This succeeds because we explicitly take a reference.
Note that I have explained this for future reference – your current code does not fall foul of this problem.
如果我理解正确,您正在从 TNode.Destroy 调用 UnregisterNode():
当 INode 位于末尾时,您可能会执行此操作它的生命周期,即当它的引用计数为 0 时。
如果UnregisterNode不采用const参数,则_AddRef将在Self,将引用计数恢复到 1,并且在 UnregisterNode 结束时,将完成 _Release,这将使引用计数恢复下降到 0,这意味着再次调用 Destroy,并且存在间接递归循环,导致堆栈溢出。
如果UnregisterNode采用const参数,则不会执行_AddRef,也不会执行_Release,所以你不会' t 进入递归循环。
如果您确保您的RegisterNode正确保留该节点,即增加其引用计数并保持这种方式,即将其存储在类型安全列表中,例如TList<,则不会发生此类问题;INode>。
If I understood you right, you are calling UnregisterNode() from TNode.Destroy:
You probably do this when an INode is at the end of its life, i.e. when its refcount is 0.
If UnregisterNode does not take a const parameter, an _AddRef will be done on Self, bringing the refcount back up to 1, and at the end of UnregisterNode, a _Release will be done, which bring the refcount back down to 0, which means that Destroy is called again, and there is your indirect recursive loop, causing a stack overflow.
If UnregisterNode takes a const parameter, no _AddRef, and no _Release are performed, so you won't get into the recursive loop.
Such problems can't happen if you make sure that your RegisterNode properly retains the node, i.e. increments its reference count and keeps it that way, i.e. stores it in a type safe list, e.g. TList<INode>.