在 Delphi 中,为啥传递接口变量有时需要它是 const 参数?
Posted
技术标签:
【中文标题】在 Delphi 中,为啥传递接口变量有时需要它是 const 参数?【英文标题】:In Delphi, why does passing a Interface variable sometimes require it to be a const parameter?在 Delphi 中,为什么传递接口变量有时需要它是 const 参数? 【发布时间】:2011-11-30 05:58:17 【问题描述】:首先是问题:为什么在UnregisterNode()
中删除 const 会导致失败,而在RegisterNode()
中则不会。
现在背景:我在 Delphi XE 中使用接口工作,我遇到了一个让我暂停的工件,我得出的结论是我真的不明白为什么。
作为接口访问的对象不需要显式释放。当最后一个引用超出范围时,它会被销毁。这似乎很简单。我编写了一个测试用例来显示按预期运行的变体和两个失败的变体。六个测试用例仅限于 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 ***
MyNode2 := nil;
end;
end.
【问题讨论】:
【参考方案1】:如果我没听错的话,你是从 TNode.Destroy 调用 UnregisterNode():
destructor TNode.Destroy;
begin
...
UnregisterNode(Self);
...
end;
您可能会在 INode 生命周期结束时执行此操作,即当它的引用计数为 0 时。
如果 UnregisterNode 不 采用 const 参数,则会在 上完成 _AddRef Self,将 refcount 恢复为 1,在 UnregisterNode 结束时,将执行 _Release,将 refcount 恢复为 0 ,这意味着再次调用Destroy,并且有你的间接递归循环,导致堆栈溢出。
如果UnregisterNode采用const参数,则不执行_AddRef,也不执行_Release,所以你不会进入递归循环。
如果您确保您的 RegisterNode 正确保留节点,即增加其引用计数并保持这种方式,即将其存储在类型安全列表中,则不会发生此类问题,例如TList
【讨论】:
这确实是我们有堆栈溢出的原因,但这段代码还有很多事情要做,这只是冰山一角。【参考方案2】:接口的引用计数
您最初的问题和 cmets 中对此答案的跟进都取决于 Delphi 的接口引用计数机制。
编译器发出代码来安排对接口的所有引用都被计算在内。每当您获取新的引用时,计数就会增加。每当释放引用(设置为nil
、超出范围等)时,计数就会减少。当计数达到零时,接口被释放,在您的情况下,这就是在您的对象上调用Free
。
您的问题是您通过将接口引用放入和取出TList
通过转换为Pointer
并返回来欺骗引用计数。在某个地方,参考文献被误算了。我确信可以解释您的代码的行为(即堆栈溢出),但我不愿意尝试这样做,因为代码使用了明显不正确的结构。
简单地说,您永远不应该将接口转换为像Pointer
这样的非托管类型。每当您这样做时,您还需要控制丢失的引用计数代码。我可以向你保证,这是你不想承担的!
您应该使用正确的类型安全容器,例如TList<INode>
,甚至是动态数组,然后引用计数将得到正确处理。对您的代码进行此更改可以解决您在问题中描述的问题。
循环引用
但是,仍然存在一个大问题,正如您自己发现并在 cmets 中详述的那样。
一旦你遵循引用计数规则,你就会面临循环引用的问题。在这种情况下,节点持有对容器的引用,而容器又持有对节点的引用。像这样的循环引用不能被标准引用计数机制破坏,您必须自己破坏它们。一旦你打破了构成循环引用的两个单独引用之一,框架就可以完成剩下的工作。
对于您当前的设计,您必须通过在您创建的每个 INode
上显式调用 UnReg
来打破循环引用。
代码的另一个问题是您使用表单的数据字段来保存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;
通过这些更改,代码运行时不会发生内存泄漏——在 .dpr 文件的开头设置ReportMemoryLeaksOnShutdown := True
以进行检查。
必须在每个节点上调用UnReg
将是一种绑定,因此我建议您只需向IContainer
添加一个方法即可。一旦您安排容器能够删除其引用,那么您将拥有一个更易于管理的系统。
您将无法让引用计数为您完成所有工作。您需要显式调用IContainer.UnRegAllItems
。
你可以像这样实现这个新方法:
procedure TContainer.UnRegAllItems;
begin
while NodeList.Count>0 do
NodeList[0].UnReg;
end;
引用计数错误
虽然 Delphi 引用计数机制总体上实现得非常好,但据我所知,存在一个长期存在且众所周知的错误。
procedure Foo(const I: IInterface);
begin
I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
当Foo
以这种方式调用时,不会生成任何代码来添加对接口的引用。因此,该接口在创建后立即释放,Foo
作用于无效接口。
因为Foo
接收参数为const
,所以Foo
不引用接口。该错误位于调用Foo
的代码生成中,它错误地没有引用接口。
解决此特定问题的首选方法如下:
var
I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
这成功了,因为我们明确地获取了一个引用。
请注意,我已对此进行了解释以供将来参考 - 您当前的代码不会遇到此问题。
【讨论】:
它不是投射到Poiner
本身是什么打破了引用计数,而是如何做到这一点。在这种情况下,重要的是(不)使用 const 参数 - 在 const 参数的情况下,RefCount
不会递增,因此如果将它与强制转换混合使用,您可能会错过一个 _AddRef()
@ain 是的,但是如果你遵守规则,那么你就不会遇到这样的问题。
@Hefferman - 我想让容器更新节点,所以它需要一个它们的列表。有没有更好的方法来处理这方面的问题?或者是我应该使用 TNode 类型而不是 INode 传递它们的共识。
只需使用 Generics.Collections 中的 TList<INode>
。不要投射到指针一切都会好起来的。根本不要使用 TNode。只使用 INode,否则你会遇到类似的问题。
@Hefferman - 我做了你建议的更改,我可以将未修改的 INode 添加到列表中。但是我必须在此过程中更改了其他内容,因为从未调用过 Destroy() 方法,并且 TList 已泄露。即使我在退出程序时手动将所有 INodes (调用 INode.Destroy() )留下一个空的 TList ,也不会调用 IContainer.Destroy() 。我会在上午看得更清楚。【参考方案3】:
参数上的 const 指令表示过程/函数不会修改该参数中提供的值。如果过程或函数希望操作任何 const 参数,它首先必须将该值复制到局部变量。
这允许编译器对这些参数执行一些优化,特别是在引用类型领域,如字符串和接口等。
特别是对于接口,由于参数被声明为 const,因此在参数的“生命周期”期间不可能修改传递的接口引用的值(因为编译器将拒绝任何代码尝试修改值),因此编译器能够消除对 AddRef() 和 Release() 的调用,否则这些调用会生成为 prolog 和 epilog那个程序。
但是请注意,在过程主体中,如果将引用分配给其他变量,则引用计数仍可能发生变化。 const 优化简单地消除了对 one AddRef/Release 对的可能需求。
const 和非const 参数之间的引用计数行为差异显然会产生一些副作用或与代码中其他复杂性的其他交互,但现在了解const 的影响,您也许能够确定您在其他地方可能出错的方式/位置。
事实上,我可以告诉你哪里出错了。 :)
除非你非常非常确定你在做什么,否则你不应该直接将接口引用投射到任何其他类型(接口或指针或其他类型)。您应该始终使用 as 或 QueryInterface() 从一种接口类型转换为另一种:
otherRef := fooRef as IOther;
并且您应该始终使用 IUnknown(或 IInterface)作为“无类型”接口引用,而不是指针。这可以确保您的参考资料都包含在所有财产中。 (有时您需要一个不计数的引用,因此会使用类型转换指针引用,但那是非常高级巫术)。
在您的示例代码中,在 TList 中维护它们的 pointer 类型的转换正在颠覆引用计数机制,并与 中的变化相结合>const/non-const 参数会导致您看到的副作用。
要维护对列表中接口的正确计数的引用,请使用接口友好的列表类,例如 TList
脚注:
还要注意:当接口引用计数降至零时,对象的销毁不一定像您想象的那样自动。
它是特定接口对象类的实现细节。如果您检查 TInterfacedObject 上的 _Release() 实现的源代码,您将看到这是如何实现的。
简单地说,当它自己的引用计数达到零时,对象本身负责销毁自己。事实上,对象甚至首先负责实现引用计数!因此,一个专门的类完全有可能(有时是可取的)覆盖或替换这种行为,在这种情况下,它如何响应零引用计数(或者实际上它是否甚至费心维护一个引用计数)完全取决于自己的需要。
话虽如此,绝大多数实现接口的对象几乎肯定会使用这种形式的自动销毁,但不应简单地假设。
应该可以安全地假设,如果您获得一个对象的接口引用,您通常不会关心该对象最终将如何被销毁。但这并不是说你可以假设它会在接口引用计数为零时被销毁。
我之所以提到这一点,是因为了解所有这些明显的“编译器魔法”是如何工作的,这对于理解您在本例中遇到的问题至关重要。
【讨论】:
当然,作为 const 传递的接口的引用计数可以改变。如果将它分配给函数内部的另一个引用,例如myIface := constParamIface;
,则增加引用计数。但编译器确实可以省略对 _AddRef 和 _Release 的调用。
嗯,是的;该部分的措辞不是很清楚,但我希望对正在发生的事情的解释足以说明这一点。事实上,我更新了该特定领域的答案,希望能进一步阐明这一点。谢谢你叫出来。
我会接受这个作为正确答案,因为它最直接地回答了指定的问题。但我真的很感谢 David 和 Deltics 的输入让我比昨天更聪明了一点。这些细节中的非常有价值的信息。
@Deltics 我认为“事实上,我可以告诉你哪里出错了。:)”是针对我的,是对我第三段中评论的引用。你有没有计算出堆栈溢出的精确机制?我必须承认我从来没有一直遵循它,因为当裁判计数如此明显时,我觉得没什么意义。
@David,不——“事实上……”不是针对任何人或任何事的。鉴于在另一个答案的 cmets 中提出了选角问题,我打算将我的答案留在原处。然后,在“意识流”时刻,我决定在答案中突出显示该问题的这一方面,以供未来读者阅读问题/答案(不是每个人都阅读 cmets)。就这些。 :)以上是关于在 Delphi 中,为啥传递接口变量有时需要它是 const 参数?的主要内容,如果未能解决你的问题,请参考以下文章
Delphi - 为啥 ExplicitWidth 和 ExplicitHeight 不断出现在 .DFM 文件中,它是啥?
Delphi - 为啥 ExplicitWidth 和 ExplicitHeight 不断出现在 .DFM 文件中,它是啥?