哪个更好:Free 还是 FreeAndNil?

52

Free和FreeAndNil之间的基本区别是什么?

FreeAndNil = Free + Nil吗?

何时应该使用Free,何时应该使用FreeAndNil?


@Sertac 这个线程和许多其他线程一样消失了。 - mjn
1
新的讨论在非技术区,分别在这里和这里。 - mjn
@mjn:几周前服务器崩溃后,许多旧的帖子消失了。您已经指出了关于该主题的新帖子。 - Rudy Velthuis
4
我怎么从未看到过这个问题? :-) - Nick Hodges
哇,48个投票和11个书签。这个小函数周围有太多的神话传说了。也许一个适当的Delphi帮助页面/文档可以澄清一些迷雾。 - Gabriel
https://blogs.embarcadero.com/freeandnil-delphi-developer-debate/ - Gabriel
5个回答

51

请参考以下实现:

看一下这个实现:

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

例子

考虑以下代码:

procedure TForm1.FormCreate(Sender: TObject);
var
  bm: TBitmap;
begin
  bm := TBitmap.Create;
  bm.LoadFromFile('C:\Users\Andreas Rejbrand\Documents\RAD Studio\6.0\Demos\DelphiWin32\VCLWin32\Football\up.bmp');
  bm.Free;

  if Assigned(bm) then
    bm.SaveToFile('C:\Users\Andreas Rejbrand\Desktop\test.bmp')
  else
    ShowMessage('Cannot save! The bitmap does no longer exist!');
end;
这将在我的桌面上创建一个错误或无效(空)位图,因为我尝试使用已被释放的对象。是的,即使bm 已经被释放了,它仍然“被分配”,也就是说,bm 仍然指向一个内存地址,即使那里没有任何(可用的)内容。为了克服这个问题,可以将bm := nil设置为保护措施。 然后,assigned(bm)将返回false,正如人们所希望的那样。更或多或少地说,FreeAndNil(bm)bm.Free; bm := nil的简写。第一条语句释放了所有内存(以及对象使用的操作系统资源、CPU时间等),bm := nil将指针bm设置为nil,以便bm不再指向对象曾经存在但现在不存在的位置。这样你(和像assigned这样的例程)就不会被欺骗认为还有一个位图对象。
讨论:有人说,你应该总是使用FreeAndNil(foo)而不是foo.Free。好的,为什么不呢?额外的指令foo := nil可能不会花费太多的纳秒级执行时间,而且确实assigned(foo) = false是一个被释放对象的非常好的属性。但是反过来,如果你知道自己在做什么,并且知道在释放后永远不会再次使用foo对象,那么你可以坚持只使用foo.free。事实上,有人认为,在许多情况下(但并非所有情况),尝试使用已释放对象的变量本身就是一个错误。(当然,有些情况下你会故意这样做 - 你有一个有时被分配的对象foo,有时不被分配。)

5
有人认为在许多情况下(但不是所有情况),尝试使用已被释放的对象本身就是一个错误,这总是一个错误,而不是“有时”。变量和对象是不同的实体:变量有时可能被赋值为“nil”,但这不是一个“nil”对象,只是一个未分配的变量。 - Cosmin Prund
1
@Cosmin - 不完全正确。未赋值的变量很少会是NIL,除非它们是字符串或接口引用。另一方面,未赋值的类成员肯定是NIL,除非您有意覆盖NewInstance实现。 - Deltics
6
我认为程序员应该在不再引用foo对象时使用foo.Free来清除它,并在需要设置为nil时使用FreeAndNil(foo)。不要在可以使用foo.Free的情况下使用FreeAndNil(foo),因为这会让读者误以为foo必须被设置为nil。 - Ian Goldby
1
当然,使用已被释放的对象是错误的。毫无疑问。但真正的问题是,当您实际这样做时会发生什么(在现实世界中,人们会犯错误):如果它是一个NIL指针,则行为是a)非常可预测(您将获得NPE),并且b)大多数情况下容易发现。通过非nil的无效指针进行写访问可能a)损坏您的系统(=安全问题),并且b)可能极其难以找到。如果某人的首要任务是“正确”,则您的愿望已经成真,但我建议您做好准备。您需要支付附带的费用。 - JensG
抱歉大家(特别是@Ian)- 讨论在foo.Free之后将foo设置为nil是否必要,我认为这只是纠缠小节、吹毛求疵,并且非常接近荒谬。我无法想象有任何一种情况不正确地清理资源是有意义的,至少也应该具有一些意义。对于那些认为这是值得注意甚至讨论的问题的人,请坚定自己的立场,放弃Delphi或任何其他高级语言并且坚持使用汇编语言。 - Semanino
显示剩余4条评论

21
基本上,FreeAndNil将引用设置为 nil ,然后释放对象。这标记为未分配。因此,您需要使用FreeAndNil的唯一原因是如果您的代码将重用引用。如果您在析构函数或 finally 块中,释放您永远不会再次触摸的对象,只需使用Free。

请参阅Delphi Memory Management Made Simple,以获取我发现它有用的示例。底部的Mghie评论也值得阅读。


4
实际上,它将引用放在本地变量中,将引用值设为 nil,然后使用本地变量释放对象。FreeAndNil 应该被命名为 NilAndFree… :) - Marjan Venema
没错。不完整的析构函数和FreeAndNIL()不是好朋友。 - Deltics
1
@Marjan,最近我遇到了一个情况,在过于复杂的单例实现中访问了从已被“FreeAndNil”处理过的析构函数中的类变量,可能会浪费我的一些时间来识别错误。 ;) - Oliver Giesen
@mason - 不使用FreeAndNil的原因是因为“它不是类型安全的”吗?得了吧。在Delphi中有很多东西都不是类型安全的。我们应该全部停止使用它们吗?顺便说一下,FreeAndNil的安全性在Sydney(或Rio?我记不清了)得到了显著提高。 - Gabriel

10

我会用另一种方式回答。

也许,在释放对象后用 nil 来填充对象引用并不总是一个好主意。

这样,您无法区分从未使用过的引用(因此为 nil)和已被使用但将来不应再使用的引用。

因此,将其填充为特殊数字(类似于 FastMM 内存管理器在释放内存块的内容时可以执行的操作)。

--jeroen


只有在程序逻辑明确依赖于那些“创建对象一次,处理并永不重新创建”的逻辑时,这个语句才有意义。这是……不寻常的……你能举一个你实际使用过这种技术的例子吗? - Gabriel
请问您能否展示一个小的可编译项目,在其中必须在释放后使用对象吗? - Gabriel
@Z80 重点是项目不应该这样做,但有些项目确实存在错误。等我完全康复(请参阅个人资料)后,我计划编写一个FastMM会议议程,介绍如何检测这些情况。然后代码应该最终放在https://github.com/jpluimers/Conferences中。 - Jeroen Wiert Pluimers
@Z80 我的论点基本上就像 SQL null 讨论一样:它意味着什么,以及在什么时间点。其中一个写作是在这里:https://datatechnologytoday.wordpress.com/2017/07/18/what-the-null-handling-missing-information-in-database-systems/ 区分它们可以帮助故障排除甚至防止错误。从某种意义上说,这是一个升级到 https://en.wikipedia.org/wiki/Fuzzing 的步骤。 - Jeroen Wiert Pluimers
请告诉我们,当您有一段代码用FreeAndNil替换Free时,程序实际上会崩溃。 - Gabriel
显示剩余4条评论

7

我非常喜欢你在SO上的工作,但这次我不喜欢你的回答。它似乎在推广“不要使用FreeAndNill”阵营。然而,像往常一样没有提供证据:你提供的三个链接都失效了!唯一一个还在运作的链接指向的文章实际上是支持FreeAndNil的(没错,不是直接支持;它建议使用FreeAndInvalidate代替FreeAndNil)! - Gabriel

3
即使看起来与Free没有太大区别,但使用FreeAndNil在查找代码中的错误时会帮助您很多。 它如何拯救您的程序(释放后访问)
想象一下,如果您不使用FreeAndNil,然后在代码的某个地方访问已释放的对象会发生什么。如果您非常幸运,当程序运行到家时立即崩溃(是的,这种崩溃是“好”的崩溃)。如果您有点不幸,它将在客户计算机上崩溃。
但这还不是全部。如果您非常不幸,程序将不会立即崩溃。或者它永远不会崩溃,而是读取或甚至更糟的写入随机内存块。这就是真正的痛苦开始的地方! 它会损害您的程序吗?
正如其他人已经指出的那样,FreeAndNil本身并不坏。FreeAndNil函数没有损坏/过时或类似于此的东西,它不会破坏您的代码。 它是一个合法的RTL函数,可以释放对象,然后将其指针设置为NIL。

事实 vs 观点
有人会争辩说依赖 FreeAndNil 可能会导致或表明设计缺陷,但是他们无法以任何方式证明这一点。这只是他们的个人观点。另一方面,一个因为你拒绝使用 FreeAndNil 而随机崩溃并且无法调试的程序是一个事实而不是观点。

连在本地变量上都要用吗?
我甚至在本地变量上也使用 FreeAndNil。在本地变量上使用它似乎是毫无意义的,因为该变量在退出过程时就消失了。
想象一下,你可能在稍后的时间添加更多代码到过程的结尾,在释放对象的位置之后;该代码将(意外)尝试访问已释放的对象。如果对象为 NIL,则会立即发生 AV 错误(example)。

一个资源猪?
一些超保守的程序员可能会说,这个简单的调用会浪费RAM和CPU资源。我也喜欢交付小型/快速/单体应用!
但我认为移除使用FreeAndNil不会浪费多少字节的RAM和CPU周期。在日常生活中,这不会有实际影响。

当你考虑到单个图形资源(如TButton图标或程序图标)可以占用50-300KB(而且你的程序可能有数十个这样的资源)时,争论FreeAndNil“浪费”的资源是毫无意义的。

神 vs 程序员
一些人可能会说,他们从未访问过一个空对象(基本上意味着他们从未犯过错误),所以在这种情况下他们不需要FreeAndNil。好吧,我不是机器人。我会犯错误。我不怕承认。

优点
Mason Wheeler已经指出了一篇文章,展示了我们都曾经使用过的使用FreeAndNil的“懒惰创建”方法:http://tech.turbu-rpg.com/106/delphi-memory-management-made-simple

缺点
没有实际/可靠的证据。

Allen Bauer有一篇文章,名为当FreeAndNil成为你的敌人时的案例,建议在非常特殊的情况下(视觉组件的析构函数)使用FreeAndNil可能会有问题。该文章中没有任何可编译的代码,只有一些可能导致问题的情况。

Allen指出应该用更好的方法替换FreeAndNil。例如使用FastMM。
然而这个讨论展示了(通过可编译的代码)FastMM失败了,而FreeAndNil挽救了全局。


证据在哪里?
此时此刻,我希望有一个具体的/可编译的代码片段来证明FreeAndNil可能会造成伤害。
另一方面,我们确实有很多例子(见链接),说明FreeAndNil可以拯救我们的生命。

对于那些反对FreeAndNil的人,请通过发布一些可编译的代码来启发我们。


3
在方法中有本地变量时,“Always”是错误的。因为该变量只能在本地范围内访问,所以很少需要使用FreeAndNil。如果您无法“看到”您不需要在那里使用FreeAndNil,则需要重构代码以减少该方法中的行数。除非您完全可以证明没有例外规则,否则绝对不应该给出“总是”的答案。 - Ken White
不,你永远不会“意外”将本地变量升级为全局变量。使用全局变量应该是一件需要仔细考虑的罕见事情。使用FreeAndNil来遮盖可能的粗心编程只是另一种形式的粗心编程。任何函数调用都有一些开销(即使它可以忽略不计),而尽管影响可能很小,但仍然是不必要的CPU周期、内存分配等。所以我的投票仍然有效。 - Ken White
如果你不能说你从来没有意外将本地变量升级为全局变量,而你的全职工作是编写代码,那么你应该小心地离开键盘,找一份新工作。我可以诚实地说,在过去的十年中,我从来没有意外地将本地变量升级为全局变量,并在使用该本地变量的方法中遇到问题,而正确的解决方案是FreeAndNil。我有几个生产应用程序,总共超过500万行代码,对源代码进行grep,发现我的代码中只有两个FreeAndNil实例。 - Ken White
4
Ken,我们明白你的观点了 :) 你踩了我的帖子,我完全接受。我不会抗议,任何人都有权维护自己的个人意见。有些人同意起诉 FreeAndNil,有些人则不同意。没有必要那么充满仇恨。 - Gabriel
通常情况下,每隔几年(或每当Delphi被出售给新的所有者时?)网站的内容就会被清除(或移动),因此链接将不再有效。因此,Allen Bauer文章的链接已经失效了。 - Gabriel
显示剩余4条评论

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