有人可以解释Delphi中的弱引用吗?
我注意到在我仔细研究的一些库/框架源代码中经常提到这个概念。我陷入了困境,想对它有一个清晰的了解。
通过接口引用相互引用的实例在基于引用计数的接口实现中相互保持活动状态。
弱引用用于打破“让彼此活着”的熊抱。这是通过将一个引用声明为纯指针来规避引用计数机制来完成的。
IFriend = Interface(IInterface)
end;
TFriend = class(TInterfacedObject, IFriend)
private
FFriend: IFriend;
end;
var
Peter: IFriend;
John: IFriend;
begin
Peter := TFriend.Create;
John := TFriend.Create;
Peter.Friend := John;
John.Friend := Peter;
end;
即使当 Peter 和 John 超出范围时,它们的实例也会被保留,因为它们的相互引用使它们的引用计数不会下降到零。
这个问题更常见于复合模式(父-子关系),其中子有对父的反向引用:
ISomething = Interface(IInterface)
end;
TSomething = class(TInterfacedObject, ISomething)
end;
TParent = class(TSomething)
FChildren: TInterfacedList;
end;
TChild = class(TSomething)
FParent: ISomething;
end;
再次,父母和孩子可以互相保持联系,因为他们的相互引用使他们的引用计数不会下降到零。
这可以通过以下方式解决weak reference
:
TChild = class(TSomething)
FParent: Pointer;
end;
通过将 FParent 声明为“纯”指针,引用计数机制不会对父级的反向引用起作用。当父级超出范围时,它的引用计数现在可以降为零,因为它的子级不再保持其引用计数高于零。
注意此解决方案确实需要仔细注意生命周期管理。当这些类“外部”的某些东西保持对孩子的引用时,孩子们可以在父母的生命之外保持活力。当孩子假设父引用总是指向一个有效的实例时,这可能会导致各种有趣的 AV。如果您需要它,请确保当父级超出范围时,它会使子级在其自己对其子级的引用之前将其反向引用为零。
默认情况下,在 Delphi 中,所有引用都是:
pointer
和class
实例的弱引用;integer, Int64, currency, double
or record
(以及旧的不推荐使用的object
or shortstring
);string, widestring, variant
interface
具有实例引用计数的强引用;强引用计数的主要问题是潜在的循环引用问题。当一个interface
对另一个有强引用,但目标interface
有一个指向原始的强指针时,就会发生这种情况。即使删除了所有其他引用,它们仍然会相互保留并且不会被释放。这也可以通过一系列对象间接发生,这些对象链中的最后一个对象可能指向较早的对象。
例如,请参见以下接口定义:
IParent = interface
procedure SetChild(const Value: IChild);
function GetChild: IChild;
function HasChild: boolean;
property Child: IChild read GetChild write SetChild;
end;
IChild = interface
procedure SetParent(const Value: IParent);
function GetParent: IParent;
property Parent: IParent read GetParent write SetParent;
end;
以下实现肯定会泄漏内存:
procedure TParent.SetChild(const Value: IChild);
begin
FChild := Value;
end;
procedure TChild.SetParent(const Value: IParent);
begin
FParent := Value;
end;
在 Delphi 中,最常见的引用复制变量(即变量、动态数组或字符串)通过实现写时复制来解决这个问题。不幸的是,这种模式不适用于接口,接口不是值对象,而是引用对象,与实现类绑定,无法复制。
请注意,基于垃圾收集器的语言(如 Java 或 C#)不会遇到此问题,因为循环引用由它们的内存模型处理:对象的生命周期由内存管理器全局维护。当然,它会增加内存使用,由于分配和分配期间的额外操作(所有对象及其引用必须在内部列表中维护)而减慢进程,并且可能会在垃圾收集器进入操作时减慢应用程序。
对于没有垃圾收集的语言(如 Delphi),一种常见的解决方案是使用弱指针,通过它将接口分配给属性而不增加引用计数。为了轻松创建弱指针,可以使用以下函数:
procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface);
begin
PPointer(aInterfaceField)^ := Pointer(aValue);
end;
因此,它可以这样使用:
procedure TParent.SetChild(const Value: IChild);
begin
SetWeak(@FChild,Value);
end;
procedure TChild.SetParent(const Value: IParent);
begin
SetWeak(@FParent,Value);
end;
您可以尝试阅读我关于 Delphi 中弱引用的博客文章- 及其相关源代码:我们已经实现了直接弱引用,以及从 Delphi 6 到 XE2 的“归零”弱引用接口处理。
实际上,在某些情况下nil
,如果您在其子实例之前释放引用实例,则需要将接口弱字段设置为 ,以避免任何访问冲突问题。这称为“归零弱指针”,Apple 使用 ARC 模型实现的,我们尝试在 Delphi 中实现。
在最一般的情况下,astrong reference
控制被引用实例的生命周期,而 aweak reference
不控制。该术语weak reference
可用于垃圾收集器、引用计数接口或公共对象的上下文中。
例如,一个 Delphi 表单包含对其所有控件的引用;这些引用可以称为强引用,因为当一个表单被销毁时,它的控件也会被销毁。另一方面,Delphi 窗体的控件具有对其所属窗体的引用。这个引用可以称为弱引用,因为它不以任何方式控制表单的生命周期。