"弱参考":需要脚踏实地的解释



有人可以提供德尔菲中弱引用的解释吗?

注意到这个概念经常在我仔细检查的一些库/框架源代码中被提及。我处于困境,想对它有一个清晰的理解。

通过接口引用相互引用的实例在基于引用计数的接口实现中保持彼此的活动状态。

用弱指法打破"让对方活着"的熊抱。这是通过将一个引用声明为纯指针来绕过引用计数机制来完成的。

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;

通过将 FParet 声明为"纯"指针,引用计数机制不会对父级的反向引用发挥作用。当父项超出范围时,其引用计数现在可以降至零,因为它的子项不再将其引用计数保持在零以上。

注意:此解决方案需要特别注意生存期管理。当这些课程的"外部"某些东西保留对孩子的引用时,孩子可以在父母的一生之后保持生命。当子引用假定父引用始终指向有效实例时,这可能会导致各种有趣的 AV。如果需要,请确保当父项超出范围时,它会先使子项取消其后备引用,然后再将其自身对其子项的引用清零。

默认情况下,在 Delphi 中,所有引用都是:

  • pointerclass实例的弱引用;
  • 低级值类型(如 integer, Int64, currency, doublerecord(的显式复制(以及旧的已弃用的objectshortstring(;
  • 写入时复制,具有高级值类型的引用计数(例如 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(,一种常见的解决方案是使用 Weak 指针,通过该指针,接口被分配给属性而不增加引用计数。为了轻松创建弱指针,可以使用以下函数:

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中实现的内容。

参见

德尔福移动编译器中的自动引用计数

其中包括新[weak]属性的文档:

ARC 的另一个重要概念是弱引用的作用, 您可以通过使用 [weak] 属性标记它们来创建。

在最一般的情况下,strong reference控制引用实例的生存期,而weak reference则不控制。术语 weak reference 可用于垃圾回收器、引用计数接口或公共对象的上下文。

例如,Delphi 表单保存对其所有控件的引用;这些引用可以称为强引用,因为当窗体被销毁时,它的控件也会被销毁。另一方面,Delphi 窗体的控件具有对其所属窗体的引用。此引用可以称为弱引用,因为它不以任何方式控制窗体的生存期。

相关内容

  • 没有找到相关文章

最新更新