variable = null as “对象破坏”从何而来?

Posted

技术标签:

【中文标题】variable = null as “对象破坏”从何而来?【英文标题】:Where did variable = null as "object destroying" come from? 【发布时间】:2011-03-09 02:48:44 【问题描述】:

在许多不同公司使用不同版本的 .NET 编写的许多遗留系统上工作,我一直在寻找以下模式的示例:

public void FooBar()

    object foo = null;
    object bar = null;

    try
    
       foo = new object();
       bar = new object();

       // Code which throws exception.
    
    finally
    
       // Destroying objects
       foo = null;
       bar = null;
    


对于任何了解 .NET 中内存管理如何工作的人来说,这种代码是非常不必要的。垃圾收集器不需要您手动分配null 来告诉旧对象可以被收集,分配null 也不会指示GC 立即收集该对象。

这种模式只是噪音,让人更难理解代码试图实现的目标。

那么,为什么我一直在寻找这种模式呢?有学校教授这种做法吗?是否有一种语言需要将 null 值分配给本地范围的变量才能正确管理内存?明确分配null 是否有一些我没有意识到的额外价值?

【问题讨论】:

来自一个不了解 C# 如何在超出范围时删除对事物的引用的人。 人们会停止为此责备 C++ 程序员吗?在 C++ 中,在这种情况下甚至不会进行堆分配,因此根本没有指针。此外,您还没有提供足够的代码来证明这是 C# 中的无操作。例如,您省略的代码可能会捕获闭包中的变量,该变量在函数返回后仍然存在(例如,通过订阅事件)。 @Ben Voigt:无论代码省略,null 的赋值都不会影响垃圾回收。他们的意图是“摧毁物体”,这是他们未能做到的。而且我也从未在 C++ 中做过这些事情。 有了这段代码,会有很大的不同:Application.UnhandledException += Frozzle( foo, bar ); ; @Ben Voigt:除非事件处理程序未注册,否则这些捕获的变量永远不会被 GC 处理。将本地引用分配给null仍然无效。 foobar 的值保持在捕获时的状态。 【参考方案1】:

这是 FUDcargo cult programming(感谢Daniel Earwicker),由习惯于“释放”资源、糟糕的 GC 实现和糟糕的 API 的开发人员编写。

一些 GC 不能很好地处理循环引用。要摆脱它们,您必须在“某处”打破循环。在哪里?好吧,如果有疑问,那么无处不在。这样做一年,它就会触手可及。

还将字段设置为null 会给您“做某事”的想法,因为作为开发人员,我们总是害怕“忘记某事”。

最后,我们有必须明确关闭的 API,因为没有真正的语言支持说“当我完成它时关闭它”并让计算机像使用 GC 一样计算出来。所以你有一个 API,你必须调用清理代码和你不需要的 API。这很糟糕并鼓励了上述模式。

【讨论】:

感谢循环引用的提示,这实际上是在旧的 VB 和类似的引用计数系统中手动取消变量的最可能的解释。 同样的情况也发生在 Python 的早期版本中。在 Java 中,所有渴望 free() 的 C/C++ 程序员或试图避免重用已破坏资源的 C++ 人员都看到了相同的模式。我经常将它用于相同的目的:resultSet = DBUtil.close(resultSet) 我会接受的。需要分配 null 以逃避旧 GC 中的循环引用听起来是一种有用的做法。 这个必须明确清理对象的业务让我想起了IDisposable。虽然我猜想使用一个实现IDisposable 的对象,但你最好调用它的Dispose() 方法。 调用Dispose 当然是可选的。如果你不这样做,GC 会有效地为你做这件事,而不是在你想要的时候。而using 只有当您的一次性物品没有离开您的范围时才是正确的做法。反例请参见 Rx 的 IObservable.Subscribe【参考方案2】:

它可能来自 VB,它使用引用计数策略进行内存管理和对象生存期。设置对Nothing 的引用(相当于null)会减少引用计数。一旦该计数变为零,则该对象被同步销毁。离开方法的作用域时计数会自动递减,因此即使在 VB 中,这种技术也基本无用,但在某些特殊情况下,您可能希望贪婪地销毁对象,如下面的代码所示。

Public Sub Main()
  Dim big As Variant
  Set big = GetReallyBigObject()
  Call big.DoSomething
  Set big = Nothing
  Call TimeConsumingOperation
  Call ConsumeMoreMemory
End Sub

在上面的代码中,big 引用的对象在没有调用Set big = Nothing 的情况下会一直持续到最后。如果方法中的其他内容是耗时的操作或产生更多的内存压力,这可能是不可取的。

【讨论】:

我以前做过一些 VB6 和 VB4 编程,当你用完对象后将对象设置为 null 的建议无处不在,尤其是对于通过 ADO 或 DAO 进行的数据库连接。坦率地说,我认为在某些情况下需要这样做,而许多其他开发人员迷信地采用这种做法,而不是理解你可能这样做的原因。也就是说,在 VB6 中,当两个对象形成循环引用时,我确实发现有时我必须这样做。 VB 并不总是正确地拆解这些结构,并且可能会泄漏内存。 我同意。可能起源于人们从 VB6 转到 VB.net 时。然后复制/粘贴 .net 编码员将这个神话带入生产系统。【参考方案3】:

它来自 C/C++,其中明确地将指针设置为 null 是常态(以消除 dangling pointers)

调用free()后:

#include <stdlib.h>

    char *dp = malloc ( A_CONST );

    // Now that we're freeing dp, it is a dangling pointer because it's pointing
    // to freed memory
    free ( dp );

    // Set dp to NULL so it is no longer dangling
    dp = NULL;

经典的 VB 开发人员在编写他们的 COM 组件以防止内存泄漏时也做了同样的事情。

【讨论】:

公平地说,在 C++ 中将局部变量设置为 NULL 与在 C# 中一样不必要。【参考方案4】:

它在具有确定性垃圾收集且没有 RAII 的语言中更为常见,例如旧的 Visual Basic,但即使在那里也没有必要,并且经常需要打破循环引用。所以它可能真的源于糟糕的 C++ 程序员,他们到处使用哑指针。在 C++ 中,删除后将哑指针设置为 0 以防止重复删除是有意义的。

【讨论】:

这在 .Net VB 之前经常发生。我认为这通常是(当我遇到它时,无论如何)对被循环引用烧毁导致过去内存泄漏的恐惧反应。 谢谢,我完全忘记了这些循环引用。 唯一的问题:你不能用局部变量形成一个引用循环。在 Java 和 C# 以及 VB6 中重置成员变量中的引用是有意义的。 作为一个花太多时间阅读旧 VB 代码的人来说,这是该语言中一个可笑的常见习语,尤其是在处理 COM(呃,ActiveX...呃,OCX.. .) 参考资料。 @Philipp:我从来没有得到令人满意的解释……当时,我把它归结为那种在 VB 文化中普遍存在的货物崇拜心态,但事后看来可能只是因为 VB 没有提供非常好的工具来跟踪引用泄漏,并且当您使用充满隐式变量声明和重用全局变量的遗留代码怪物时,这是唯一的方法 确定您并没有在应该发布它们的时间点很久之前挂在引用上(当这些引用附加到数据库连接等时很重要)。【参考方案5】:

我在 VBScript 代码(经典 ASP)中看到了很多,我认为它来自那里。

【讨论】:

它在VBScript中有价值吗? 它基本上是空的。自从发布以来,我已经在 Google 上搜索过了,这就是这种做法的起源。在 VBScript 中,将该值设置为 none 会释放对对象的引用,从而允许回收资源(基本上是强制对象没有任何引用)。我不能 100% 确定在 ASP 中是否有必要,但我看到的每个页面都声称它是一种很好的做法。 VBScript 可能(如 VB6)具有带有引用计数的确定性内存管理,并且将某些内容设置为 null 会立即减少引用计数器。但是,我想知道这会对 VBScript 所针对的那些简单、短时运行的脚本产生什么影响。 知道这确实是正确答案,我有点难过。 它在 VB 变体中的使用非常有限。在大多数情况下,设置对Nothing 的引用是没有用的,因为当引用超出范围时会自动发生这种情况,但在某些特殊情况下,贪婪地这样做很有用。在我 VB6 的日子里,我注意到开发人员经常不必要地这样做。变化越多,它们就越保持不变:(【参考方案6】:

我认为这曾经是前 C/C++ 开发人员之间的常见误解。他们知道 GC 会释放他们的内存,但他们并不真正了解何时以及如何。只需清洁它并继续:)

【讨论】:

【参考方案7】:

我怀疑这种模式来自于将 C++ 代码转换为 C#,而没有停下来了解 C# 终结和 C++ 终结之间的区别。在 C++ 中,我经常将析构函数中的内容清空,或者出于调试目的(以便您可以在调试器中看到引用不再有效),或者很少因为我希望释放一个智能对象。 (如果这是我宁愿调用 Release 的意思,让维护者清楚地了解代码的含义。)正如您所指出的,这在 C# 中几乎毫无意义。

出于不同的原因,您也一直在 VB/VBScript 中看到这种模式。我在这里思考了一下可能导致这种情况的原因:

http://blogs.msdn.com/b/ericlippert/archive/2004/04/28/122259.aspx

【讨论】:

【参考方案8】:

可能是分配 null 的约定源于 foo 是实例变量而不是局部变量的事实,您应该在 GC 收集它之前删除引用。有人在第一句话的时候就睡着了,开始取消他们所有的变量;人群紧随其后。

【讨论】:

【参考方案9】:

它来自 C/C++,其中对已释放的指针执行 free()/delete 可能会导致崩溃,而释放 NULL 指针则什么也没做。

这意味着这个构造(C++)会导致问题

void foo()

  myclass *mc = new myclass(); // lets assume you really need new here
  if (foo == bar)
  
    delete mc;
  
  delete mc;

虽然这会起作用

void foo()

  myclass *mc = new myclass(); // lets assume you really need new here
  if (foo == bar)
  
    delete mc;
    mc = NULL;
  
  delete mc;

结论:在 C#、Java 和几乎任何其他垃圾收集语言中完全不需要 IT。

【讨论】:

分配一个指向 null 的指针并没有释放资源,它只是破坏了指向它的指针,导致内存泄漏。在 C# 中,我仍然建议使用 destroy 或 close 方法。 @Vladimir:我认为这就是我所暗示的,但是好的,更新应该澄清它。 这更像是 C# 中的 foo.Dispose(); foo = null;,而不是我们在这里讨论的内容。 但这可能仍然是它的来源。 @Ben:我们谈论的是在离开方法之前将对象设置为 null 的来源,而不是它在 C# 中的含义或将是什么【参考方案10】:

考虑稍作修改:

public void FooBar() 
 
    object foo = null; 
    object bar = null; 

    try 
     
       foo = new object(); 
       bar = new object(); 

       // Code which throws exception. 
     
    finally 
     
       // Destroying objects 
       foo = null; 
       bar = null; 
     
    vavoom(foo,bar);
 

作者可能想要确保如果先前抛出并捕获了异常,大 Vavoom (*) 不会获得指向错误对象的指针。偏执狂导致防御性编码,在这个行业不一定是坏事。

(*) 如果你知道他是谁,你就知道了。

【讨论】:

finally 块保证引用为空,无论是否发生异常。 即使 'finally' 块后面的代码都不会使用无效的 'foo' 或 'bar' 对象,在 'finally' 子句中清除它们将确保它们有资格执行“vaboom”期间的垃圾收集。在大多数情况下,对象是在 'vaboom' 之前还是之后被 gc'ed 并不重要,但如果例如'vaboom' 包含一个套接字监控线程的主循环,提前杀死 'foo' 和 'bar' 可能会有用。【参考方案11】:

VB 开发人员必须释放他们的所有对象,以尝试减少内存泄漏的可能性。我可以想象这就是 VB 开发人员迁移到 .NEt / c# 时它的来源

【讨论】:

旧 VB 将引用计数作为确定性内存管理方法。所以从来没有必要手动取消某些东西,只要变量超出范围,它就会自动完成。但有时确实会手动将变量设置为 null 以提前减少引用计数(并可能破坏它们)。【参考方案12】:

我可以看到它来自对垃圾收集如何工作的误解,或者是为了强制 GC 立即启动 - 可能是因为对象 foobar 非常大。

【讨论】:

【参考方案13】:

我以前在一些 Java 代码中看到过这种情况。它用于一个静态变量,表示该对象应该被销毁。

但它可能并非源自 Java,因为将它用于静态变量以外的任何东西在 Java 中也没有任何意义。

【讨论】:

【参考方案14】:

它来自 C++ 代码,尤其是智能指针。在这种情况下,它大致相当于 C# 中的 .Dispose()

这不是一个好的做法,最多是开发人员的本能。在 C# 中分配 null 并没有真正的价值,除了可能帮助 GC 打破循环引用。

【讨论】:

这与智能指针有什么关系?将指针设置为 null 对我来说似乎与 RAII 相反。而且它根本不等同于Dispose。像unique_ptrshared_ptr 这样的智能指针实现了确定性内存管理模型,其中资源处理由RAII 模式处理。由于 RAII 在非确定性内存管理模型中是不可能的,因此需要某种Disposable 模式。 问题的一部分是:“是否有一种语言需要将空值分配给本地范围的变量才能正确管理内存?”,是的,C++ 智能指针可以。这是一种内存管理(没有垃圾收集器)。帖子中似乎没有任何内容表明那些= null 不是来自修改后的复制/粘贴(调用后MSVC 甚至支持__finally)。 @Wernight:答案是“不,绝对不是”。在 C++ 中将智能指针设置为 NULL 绝对没有必要,当智能指针析构函数超出范围时被调用(并且在异常堆栈展开期间调用析构函数,它们会自动得到“最终”处理),它将释放目标。 @Ben:我没说有必要。调用“Dispose()”也不是必要的。在您的示例中,由于它们的范围在 tryfinally 之外,因此不会自动调用它。现在有什么用?可能只会更快地释放该对象。不管它是如何出现的(C++ 或其他),这可能是最初开发者的意图 @Ben:没有必要将智能指针设置为空除非有必要。一些 COM 对象在发布时有排序要求;例如,如果您在子流上仍有未完成的引用,则不允许您对结构化文件存储进行最终发布。我经常编写需要以精确顺序拆除对象的代码。现在,在这些情况下,我个人更喜欢使用哑指针;当指针比我聪明时,我用错了。但有时您确实有充分的理由想要明确地这样做。

以上是关于variable = null as “对象破坏”从何而来?的主要内容,如果未能解决你的问题,请参考以下文章

JS -- Variables As Properties

declare 在SQL中是啥意思,怎么用?

javascript ES6_variable_as_object_key

C++ as in Java?: 具有 *variable* int 长度的向量向量

NET问答:null != variable 和 variable != null 到底有什么区别?

GO方法集问题cannot use xxxxx as xxxxx value in variable declaration