为啥 FreeAndNil 实现在 Free 之前执行 Nil?

Posted

技术标签:

【中文标题】为啥 FreeAndNil 实现在 Free 之前执行 Nil?【英文标题】:Why FreeAndNil implementation doing Nil before Free?为什么 FreeAndNil 实现在 Free 之前执行 Nil? 【发布时间】:2014-12-24 18:34:13 【问题描述】:

如果你看一下 FreeAndNil 过程的代码,你会看到:

procedure FreeAndNil(var Obj);
var
  Temp: TObject;
begin
  Temp := TObject(Obj);
  Pointer(Obj) := nil;
  Temp.Free;
end;

他们将Nil 分配给对象引用并且仅在销毁它之后才分配的原因是什么?为什么不反过来呢?

【问题讨论】:

如果你切换并且析构函数引发了异常,你会得到一个过时的指针。无论您是否应该使用 FreeAndNil,您都可以阅读它here 并得出自己的结论。 @pf1957 - FreeAndNil 是not threadsafe,不管你怎么切换代码 @Andrew 那篇文章不是对此事的最终决定权。还有其他意见。并且不要将其视为永远不要调用 FreeAndNil 的一揽子建议。 Nick 认为,如果你按照他的方式编写代码,你就不需要 FreeAndNil。仅当您按照他所倡导的方式编写代码时,该论点才适用。并非所有代码都是这样编写的。 不重复,但是... ***.com/questions/8548843/… 我以前写过这个:cs.wisc.edu/~rkennedy/freeandnil 【参考方案1】:

我可以想到两个这样做的原因,但似乎都没有令人信服的理由。

原因 1:保证在引发异常的情况下将引用设置为 nil

实现实现了这一点。如果析构函数引发,则引用仍设置为 nil。另一种方法是使用finally 块:

try
  TObject(Obj).Free;
finally
  TObject(Obj) := nil;
end;

这样做的缺点是性能。特别是在 x86 上,try/finally 有点贵。在这样一个基本的例程中,避免这种费用是谨慎的。

为什么我觉得不惜一切代价实现零的愿望并不令人信服?好吧,一旦析构函数开始失败,你也可以放弃。您无法再清楚地推断程序的状态。您无法分辨出什么失败以及您的程序处于什么状态。我认为,面对引发的析构函数,正确的做法是终止进程。

原因2:保证其他线程可以检测到对象正在被销毁

这又实现了,但没有实际用处。是的,您可以测试是否分配了引用。但是然后呢?另一个线程不能在没有同步的情况下调用对象上的方法。您所能做的就是了解对象是否还活着。如果是这样,那么这个状态在析构函数运行之前还是之后发生变化又有什么关系呢?

因此,虽然我将此作为可能的原因,但我无法相信 Embarcadero 中的任何人真的被这种论点所左右。

【讨论】:

感谢您的解释。 要满足原因 2,FreeAndNil 本身必须是线程安全的,但事实并非如此,所以我真的只看到一个 (valid) 原因。 @LievenKeersmaekers 我已经在答案中说过了。如果没有同步,不同线程中的所有代码都可以做的是测试引用是否被分配。那将是安全的。对 nil 的赋值是发生在析构函数之前还是之后似乎是任意的。我个人认为这两个理由都不成立。 @DavidHeffernan - 你的声音很重要,从你的回答中我并不清楚。现在是。 你说得对,这两个原因都没有真正令人信服。【参考方案2】:

David's second reason 有一个更引人注目的变体。虽然有人可能会争辩说,如果它适用,还有其他问题需要解决。

原因3:确保同一线程上的事件处理程序可以检测到对象正在被销毁

这是一个虚构的假设示例:

TOwner.HandleCallback;
begin
  if Assigned(FChild) then
    FChild.DoSomething;
end;

TChildClass.Destroy;
begin
  if Assigned(FOnCallback) then FOnCallback;
  inherited Destroy;
end;

现在如果 TOwner 来电:

FChild.Free;
FChild := nil;

FChild 在其销毁周期的中间将被要求发送至DoSomething。某种灾难的秘诀。 FreeAndNil 的实现巧妙地避免了这种情况。

是的,您可能会争辩说在销毁期间触发回调事件是危险的,但它确实有其好处。 Delphi/VCL 代码中的例子很少。尤其是如果您将关注范围扩大到包括调用多态方法 - 这也将破坏序列的各个方面置于您的控制之外。

现在我必须指出,我不知道 Delphi 库代码中可能出现此问题的任何特定情况。但是在 VCL 的某些部分中存在一些复杂的循环依赖关系。因此,如果将实现更改为 SysUtils 中更明显的选择会导致一些令人不快的意外,我不会感到惊讶。

【讨论】:

似乎很假设。由于将 FChild 设置为 nil 对 TChildClass 是私有的/受保护的,因此只有在 FreeAndNil(FChild) 在其自己的 TChildClass.Destroy 析构函数中完成时才有意义。我没有看到任何能够实现这个假设的实际例子。如果您有这样的循环引用,那么同一个实例将拥有并调用 FChild 属性。 我对 FreeAndNil 和 var 处理提出了一些更改。查看quality.embarcadero.com/browse/RSP-29714 和quality.embarcadero.com/browse/RSP-29716,如果对您有意义,请投票。谢谢。 @H.Hasenack 我看不出这个 RSP 修改的相关性。在 x86 上分配指针已经是原子的了。只有当方法调用者也以原子方式交换并拥有指针时,它才有意义......但是如果没有任何引用计数,它就没有任何意义。您的代码不会更安全,而且肯定会更慢,因为它会刷新 CPU 缓存而没有任何好处。 @Arnoud Bouchez 在多线程应用程序中,使用当前实现可以在将原始指针设置为 nil 和调用 temp.free 之间切换上下文。因此 TObject.Free 可能会被不同的线程为同一个对象多次调用。当然,创建一个 ThreadSafeFreeAndNil 替代例程是不费吹灰之力的。顺便说一句:我不明白你关于引用计数的评论。我不确定性能损失,必须对此进行测试。 @Arnoud Bouchez 想在单独的线程/MS 团队中讨论这个问题吗?我已向 RSP-29714 cmets 添加了性能测试。【参考方案3】:

多年来唯一真正困扰我的是FreeAndNil(var obj) 缺乏类型安全性。我知道,由于缺乏足够的语言支持,根本无法做到正确,但由于我们有一段时间使用泛型,这里有一个快速简单的工具,可以让您的生活更轻松。

type
  TypeSafe = class sealed
  public
    class procedure FreeAndNil<T : class>( var obj : T);  static; inline;
  end;


class procedure TypeSafe.FreeAndNil<T>( var obj : T);
begin
  SysUtils.FreeAndNil( obj);
end;

如果您为普通的FreeAndNil(var Obj) 添加重载并将其标记为deprecated,您就有机会找到有人将接口指针交给它的所有地方——并且如果代码库只是很大相信我,你会发现有趣的东西。

procedure FreeAndNil(var Obj); inline; deprecated 'use TypeSafe.FreeAndNil<T>() instead';

请注意,您甚至不必指定&lt;T&gt;,因为编译器足够聪明,可以为您找到正确的类型。只需在顶部添加单位并全部更改

FreeAndNil(foobar);

在你的源代码中放入

TypeSafe.FreeAndNil(foobar);

你就完成了。

【讨论】:

您是否看到 Delphi 10.4 比使用 const [ref] 参数的提议更好地处理这个问题? 我使用辅助类实现了通用解决方案。所以我(真的)不需要“类型安全”。标题。缺点是您只能在代码 AFAIK 中的任何位置使用 1 个辅助类。 IMO const [ref] 参数也不是最佳解决方案。您不希望被调用的例程更改 const 参数。 虽然 Delphi 10.4 may handle it differently,为了类型安全牺牲一个 const 参数是否真的更好可能是值得怀疑的。当然,在 Delphi 中,const 一直只是一种“编译器提示”,与 C++ 中更严格的 const correctness 概念相比。我可以在 Delphi 中写出关于const 有什么问题的卷。但我的意思是……真的吗? 我刚刚意识到 Syndey 实现为全新的 FreeAndNil 错误类别腾出了空间。因为现在你可以 FreeAndNil() 一个属性。以前编译器会告诉您不能为 const 值分配 var 参数... doh!

以上是关于为啥 FreeAndNil 实现在 Free 之前执行 Nil?的主要内容,如果未能解决你的问题,请参考以下文章

对象释放两种方法对比:Free 和 FreeAndNil()

[原创]Delphi FreeAndNil 是一个过程,并不是函数

C语言中已经有了malloc和free,为啥还需要new和delete?

为啥在 GridUnload 之后 free-jqgrid 不重置行 ID?

为啥free函数不在释放内存后,将指针置NULL,野指针有啥用

为啥可以用空指针调用free?