11.1.4 Weak 和 Unsafe 类型接口引用
从 Delphi 10.1 Berlin 开始,Object Pascal 语言对接口引用的管理进行了优化。实际上,Object Pascal 语言提供了不同类型的引用:
-
常规引用在分配和释放时分别递增和递减对象引用计数,最终在引用计数为零时释放对象。
-
弱引用(标有[weak]修饰符)不会增加所引用对象的引用计数。这些引用是完全可管理的,因此如果被引用对象被销毁,它们会被自动设置为nil。
-
不安全引用(标有[unsafe]修饰符)不会增加其引用对象的引用计数,并且不受管理–这与基本指针没有太大区别。
注解 弱引用和不安全引用的使用最初是作为移动平台 ARC 内存管理支持的一部分引入的。由于 ARC 现已淘汰,因此该功能仅适用于接口引用。
通常引用计数在处于激活状态的情况下,你可以使用如下代码,该代码依赖引用计数来释放临时对象:
procedure TForm3.Button2Click(Sender: TObject);
var
OneIntf: ISimpleInterface;
begin
OneIntf := TObjectOne.Create;
OneIntf.DoSomething;
end;
如果对象有一个标准的引用计数实现,而您想创建一个不计入引用总数的接口引用,该怎么办?现在,您可以通过在接口变量声明中添加 [unsafe] 属性来实现这一目的,将上面的代码更改为:
procedure TForm3.Button2Click(Sender: TObject);
var
[unsafe] OneIntf: ISimpleInterface;
begin
OneIntf := TObjectOne.Create;
OneIntf.DoSomething;
end;
这并不是一个好主意,因为上述代码会导致内存泄漏。通过禁用引用计数,当变量离开作用域时,什么也不会发生。在某些情况下,这样做是有好处的,因为你仍然可以使用接口,而不会触发额外的引用。换句话说,不安全引用就像指针一样,不需要额外的编译器支持。
现在,在你打算使用[unsafe] 属性而不增加引用计数的时候,请考虑一下,在大多数情况下,还有另一种更好的选择:使用弱(weak)引用。弱引用也可以避免增加引用计数,但它们是受管理的。这意味着系统会跟踪弱引用,如果实际对象被删除,系统会将弱引用设置为nil。相反,如果使用不安全引用,则无法知道目标对象的状态(这种情况称为悬挂引用)。
弱引用在哪些情况下有用?一个典型的情况是两个对象相互引用。事实上,在这种情况下,该对象会武断地增加其他对象的引用计数,并导致对象基本上会永远被保留在内存中(随着引用计数被设置为 1),即使对象变得不可访问也是如此。
举例来说,下面的接口接受对另一个同类型接口的引用,而类通过内部引用实现这一个功能:
type
ISimpleInterface = interface
procedure DoSomething;
procedure AddObjectRef(Simple: ISimpleInterface);
end;
TObjectOne = class(TInterfacedObject, ISimpleInterface)
private
AnotherObj: ISimpleInterface;
public
procedure DoSomething;
procedure AddObjectRef(Simple: ISimpleInterface);
end;
如果创建两个对象并交叉引用,就会导致内存泄漏:
var
One, Two: ISimpleInterface;
begin
One := TObjectOne.Create;
Two := TObjectOne.Create;
One.AddObjectRef(Two);
Two.AddObjectRef(One);
现在,在Delphi中的解决方案是将私有字段 AnotherObj
标记为弱引用(weak interface reference):
private
[weak] AnotherObj: ISimpleInterface;
有了这一改动,在将对象作为参数传递给 AddObjectRef 调用时,引用计数不会被修改,而是保持为 1,当变量离开作用域时,引用计数会归零,从而将对象从内存中释放出来。
现在有了这一功能,在许多其他类似情况下都变得非常方便,尽管底层实现非常复杂。弱引用是一项重要的功能,但要完全掌握它还需要进一步的努力。此外,由于弱引用是受管理的(而不安全引用则不受管理),因此在运行时也会产生一些代价。
有关接口的弱引用及其工作原理的更多信息,请参阅第 13 章 "对象与内存 "中的 "内存管理与接口 "一节。