为什么在析构函数中应该使用Free而不是FreeAndNil?

14

我已经阅读了反对FreeAndNil的理由,但仍然不明白为什么我不能在一个类的析构函数中使用这种方法?有人能解释一下吗?

更新:我认为来自Eric Grange的评论对我最有帮助。链接表明这并不明显如何处理它,主要是品味问题。另外,FreeAndInvalidate方法也很有用。


你在这里提出了一个不错的问题。 - Gabriel
4个回答

16
问题在于许多人似乎将FreeAndNil视为能够解决神秘崩溃问题的万能药。如果在析构函数中使用FreeAndNil()似乎可以解决崩溃或其他内存损坏问题,那么你应该深入挖掘真正的原因。当我看到这个问题时,我会首先问的问题是:为什么在销毁实例之后还要访问实例字段?这通常指向一个设计问题。
这意味着您的代码正在访问已经被销毁的对象的属性/字段/方法(析构函数已经被调用)。因此,与其用FreeAndNil掩盖真正的问题,您应该真正解决潜在的问题。
下面这段代码如果在SomeObject的析构函数中使用FreeAndNil PropertyA则不会崩溃。但它掩盖了SomeObject在销毁后仍然被使用的真正问题。更好的做法是解决这个设计问题(访问被销毁的对象)而非掩盖它。
SomeObject.Free;  // Destructor called
if Assigned(SomeObject.PropertyA) then  // SomeObject is destroyed, but still used
  SomeObject.PropertyA.Method1;  

编辑

另一方面,有人可能会认为如果不使用FreeAndNil,代码也不会崩溃。因为即使对象已被销毁,内存可能尚未被重用并且所有结构可能仍然完整。如果使用Free而不是FreeAndNil来销毁PropertyA,则上面的代码甚至可能运行无误。

如果使用FreeAndNil来销毁SomeObject,无论解构函数中的代码是什么,您也将看到真正的问题。

因此,尽管我同意它可能会隐藏真正的设计缺陷,并且个人在解构函数中不使用FreeAndNil,但它不是发现这种设计缺陷的某种神奇解决方案。


如果不使用FreeAndNIL,可能会忽略的问题是,如果您的析构函数遇到异常,则会留下一个未销毁的对象,该对象可能会受到另一次销毁尝试的影响。在这种情况下,已经释放的对象当然不应再次释放。FreeAndNIL可防止不完整的析构函数。有人认为,不完整的析构函数也反映了糟糕的设计,但不幸的是,您并不总是能够精确控制析构函数中会发生什么(例如,如果您销毁其他对象,其析构函数行为并非直接由您控制或轻松更改)。 - Deltics
4
为了保护,你可以使用类似于 'FreeAndInvalidate'(http://delphitools.info/2010/02/06/dont-abuse-freeandnil-anymore/)的东西来使引用无效而不仅仅是 nil,这样所有进一步访问对象的尝试都会触发异常。 - Eric Grange
谢谢提供链接,这给了我一些启示。 - Roland Bengtsson

9
问题很容易解释,而围绕这个问题的争议更多是主观的而非客观的。如果变量引用被释放的对象将超出作用域,使用FreeAndNil是完全不必要的:
procedure Test;
var
  LObj: TObject;
begin
  LObj := TObject.Create;
  try
    {...Do work...}
  finally
    //The LObj variable is going out of scope here,
    // so don't bother nilling it.  No other code can access LObj.

    //FreeAndNil(LObj);
    LObj.Free;
  end; 
end;

在上面的代码片段中,将LObj变量设为nil是毫无意义的,原因已经给出。但是,如果一个对象变量在应用程序生命周期内可以被实例化和释放多次,那么检查对象是否确实被实例化是必要的。检查这一点的简单方法是检查对象引用是否已设置为nil。为了方便设置nilFreeAndNil()方法将同时释放资源并为您设置nil。然后,在代码中,您可以使用LObj = nilAssigned(LObj)来检查对象是否被实例化。
在对象解构函数中使用.FreeFreeAndNil()的情况是一个灰色的领域,但大多数情况下,.Free应该是安全的,而在解构函数中将子对象的引用设为nil是不必要的。关于如何处理构造函数和解构函数中的异常有各种不同的观点。
现在请注意:如果您根据上述特定情况选择使用.FreeFreeAndNil(),那么没问题,但请注意,由于未将释放的对象引用设为nil而随后访问它可能会导致错误的代价非常高。如果随后访问指针(对象已被释放但引用未设为nil),那么您可能会不幸地发现,内存损坏的检测发生在访问已释放但未设置为nil的对象引用的许多代码行之后。这种错误可能需要很长时间才能修复,是的,我知道如何使用FastMM。
因此,对于一些人,包括我自己,在释放对象指针时简单地将它们全部设为nil已经成为习惯,即使这样做并不严格必要。

你正在处理一个特定情况,其中有一个小过程,变量超出了其作用域。不仅如此。但是+1,我同意使用FreeAndNil。 - Gabriel
@Altar:我选择这个例子是因为它提供了最好的情况,可以争论“Free”比“FreeAndNil”更好。所有其他情况都不够精确。再次强调:当销毁对象实例时,我总是将我的对象引用设置为nil,这样我就永远不必考虑是否需要这样做。 - Caleb Hattingh

5
我曾经经常使用FreeAndNil(不知道为什么),但现在不再使用了。让我停止使用它的原因与变量是否需要在之后设置nil无关。它与代码更改有关,特别是变量类型的更改。
我曾经多次在将变量类型从TSomething更改为接口类型ISomething后被咬伤过。 FreeAndNil不会抱怨,并愉快地继续在接口变量上执行其工作。这有时会导致神秘的崩溃,无法立即追溯到其发生的地方,需要花费一些时间才能找到。
因此,我重新切换回调用Free。而且当我认为有必要的时候,会显式地将变量设置为nil

3
我找到了一个StackOverflow的问题,讨论了FreeAndNilFreeAndInvalidate,并提到微软安全开发生命周期现在建议使用类似于FreeAndInvalidate的东西:链接
鉴于上述SDL建议以及与重用已删除C++对象的过时引用相关的一些真实错误...... 明显的净化值选择是NULL。但是这样做有缺点:我们知道很多应用程序崩溃是由于NULL指针解引用导致的。选择NULL作为清洗值意味着此功能引入的新崩溃可能不太可能引起开发人员的注意,因为需要适当管理C++对象生命周期,而不仅仅是NULL检查,可以抑制即时症状。 此外,对NULL的检查是常见的代码结构,这意味着现有的NULL检查结合使用NULL作为清洗值可能会巧妙地隐藏一个真正需要解决的内存安全问题的根本原因。 出于这个原因,我们选择0x8123作为净化值-从操作系统的角度来看,这与零地址(NULL)在同一个内存页面中,但在0x8123处的访问冲突将更好地引起开发人员的关注,需要更详细的注意力。
因此,基本上:
procedure FreeAndInvalidate(var obj);
var
   temp : TObject;
const 
   INVALID_ADDRESS = $8123; //address on same page as zero address (nil)
begin
   //Note: Code is public domain. No attribution required.
   temp := TObject(obj);
   Pointer(obj) := Pointer(INVALID_ADDRESS);
   temp.Free;
end;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接