你需要处理对象并将其设置为 null 吗?

348

你需要处理对象并将它们设置为null,还是垃圾收集器会在它们超出范围时清理它们?


4
似乎有一致的意见认为你不需要将对象设置为null,但是你需要执行Dispose()吗? - CJ7
是的,那就对了。 - Jeremy McGee
3
正如Jeff所说的:http://www.codinghorror.com/blog/2009/01/die-you-gravy-sucking-pig-dog.html - tanathos
9
我的建议是,如果一个对象实现了IDisposable接口,就要始终进行处理。每次都使用using代码块。不要做任何猜测,也不要留给机会。虽然你不需要将对象设置为null, 但是这个对象必须离开作用域。 - peter
11
@peter:不要在WCF客户端代理中使用“using”块:http://msdn.microsoft.com/en-us/library/aa355056.aspx - nlawalker
9
然而,在您的 Dispose() 方法中,您可能需要将某些引用设置为 null!这是对这个问题微妙的变化,但很重要,因为被处理的对象不知道它是否会“超出范围”(调用 Dispose() 并不能保证)。更多信息请参见:https://dev59.com/Mmw15IYBdhLWcg3wMpEY - Kevin P. Rice
12个回答

270

当对象不再使用时,它们将被清理,垃圾收集器也会在合适的时候进行清理。有时,您可能需要将一个对象设置为null以使其超出范围(例如您不再需要其值的静态字段),但通常没有必要将其设置为null

关于释放对象,我赞同@Andre的观点。如果对象是IDisposable接口实现类,那么当您不再需要该对象时,最好将其释放,特别是当对象使用非托管资源时。未正确释放非托管资源会导致内存泄漏

您可以使用using语句,在程序离开using语句的范围时自动释放对象。

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here

这个与以下函数的功能等效:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}

4
关于 IDisposable,未能 Dispose 对象通常不会导致内存泄漏,因为任何设计良好的类都不会出现这种情况。在使用 C# 处理非托管资源时,应该编写一个终结器用于释放非托管资源。这意味着它不会在应该释放资源的时候进行解除分配,而是推迟到垃圾回收器对托管对象进行终结时才进行。然而,这仍然可能会导致许多其他问题(例如未释放的锁)。但是,您应该始终 Dispose 一个 IDisposable - Aidiakapi
@RandyLevy,你有相关的参考资料吗?谢谢。 - Basic
@Mihail 我最近没有做 C#,所以我可能有点生疏,但是如果我没记错的话,Dispose 和垃圾回收器没有什么特别之处,它只是在 C# 中释放资源的一种模式,让垃圾回收器在最终化对象之前释放资源。如果你有一个 finalizer,请确保在 dispose 中调用 GC.SuppressFinalize(this),这样 GC 就不必做比必要更多的工作了。请参阅 https://msdn.microsoft.com/library/b1yfkh5e(v=vs.100).aspx - Zach Johnson
@Mihail 你不需要担心托管资源,或将东西置空(除非它们被直接或间接地通过静态字段引用 - 可以使用内存分析器来帮助解决这个问题) - Zach Johnson
@ZachJohnson说得很有道理!非常感谢您先生!干杯! - Mihail Georgescu
显示剩余2条评论

146

C#中的对象永远不会像C++那样失去作用域,它们在不再使用时由垃圾回收器自动处理。这比C++更复杂,因为变量的作用域完全是确定性的。CLR垃圾收集器主动遍历所有已创建的对象,并判断它们是否正在使用。

一个对象可以在一个函数中“超出作用域”,但如果它的值被返回,则GC将查看调用函数是否持有返回值。

将对象引用设置为null是不必要的,因为垃圾收集是通过找出哪些对象被其他对象引用来工作的。

实际上,你不必担心销毁,它只是工作得很好,这很棒 :)

当你完成对实现了IDisposable的所有对象的操作时,必须调用Dispose。通常,您会像这样使用using块来处理这些对象:

using (var ms = new MemoryStream()) {
  //...
}

编辑:关于变量作用域的问题。Craig问变量作用域是否对对象生命周期有影响。为了正确解释CLR的这个方面,我需要解释一下C++和C#中的一些概念。

实际变量作用域

在两种语言中,变量只能在定义它的范围内使用 - 类、函数或由大括号括起来的语句块。然而,微妙的区别在于,在C#中,变量不能在嵌套块中重新定义。

在C++中,这是完全合法的:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8

然而,在C#中你会得到一个编译器错误:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}

如果您查看生成的 MSIL,这将会有意义 - 函数使用的所有变量都在函数开头定义。请看这个函数:
public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}

以下是生成的IL代码。请注意,定义在if块内的iVal2实际上是在函数级别上定义的。这实际上意味着,就变量生命周期而言,C#只有类和函数级别的作用域。

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope

C++作用域和对象生命周期

在C++中,只要分配在堆栈上的变量超出其作用域范围,它就会被销毁。请记住,在C++中,您可以在堆栈或堆上创建对象。当您在堆栈上创建对象时,一旦执行离开作用域,它们就会从堆栈中弹出并被销毁。

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives

当C++对象在堆上创建时,必须显式销毁,否则会造成内存泄漏。但对于栈变量,则没有这样的问题。

C#对象生命周期

在CLR中,对象(即引用类型)总是在托管堆上创建。这一点还通过对象创建语法得到了进一步强化。考虑以下代码片段。

MyClass stackObj;

在C++中,这将在堆栈上创建一个MyClass实例并调用其默认构造函数。在C#中,它将创建一个不指向任何内容的类MyClass的引用。创建类的实例的唯一方法是使用new运算符:
MyClass stackObj = new MyClass();

以某种方式而言,C# 对象与使用 C++ 中的 new 语法创建的对象非常相似——它们在堆上创建,但与 C++ 对象不同的是,它们由运行时管理,因此您不必担心对其进行析构操作。
由于对象始终在堆上,因此对象引用(即指针)超出作用域的事实变得无关紧要。确定是否收集对象涉及更多因素,而不仅仅是对象的引用存在。

C# 对象引用

Jon Skeet 将 Java 中的对象引用 比作附在气球上的线,而气球则是对象。同样的类比也适用于 C# 对象引用。它们只是指向包含对象的堆中的位置。因此,将其设置为 null 对对象生命周期没有直接影响,气球会继续存在,直到 GC "弹"掉它。
继续使用气球类比,似乎逻辑上讲,一旦气球没有线连接,就可以销毁它。实际上,这正是非托管语言中引用计数对象的工作原理。但是,这种方法在循环引用方面效果不佳。想象一下两个气球通过线连接在一起,但是没有一个气球连接到其他任何东西。根据简单的引用计数规则,它们都将继续存在,即使整个气球组已经"孤立"。
.NET 对象很像屋顶下的氦气球。当屋顶打开(GC 运行)时,未使用的气球会漂浮走,即使可能有一些气球被系在一起。

.NET GC使用一种混合的分代GC和标记-清除方法。分代方法涉及运行时优先检查最近分配的对象,因为它们更有可能未被使用;而标记-清除方法则需要运行时遍历整个对象图,并确定是否有未使用的对象组。这可以很好地处理循环依赖问题。

此外,.NET GC在另一个线程(所谓的终结器线程)上运行,因为它要做的事情相当多,如果在主线程上执行会中断您的程序。


1
@Igor:所谓“超出范围”,是指对象引用超出了上下文,无法在当前作用域中引用。在C#中肯定还会发生这种情况。 - CJ7
@Tuzo:你需要详细解释一下。我谈论的唯一一种作用域就是当函数完成时,使变量在函数内部局部化的作用域。还有什么其他种类的作用域吗? - CJ7
1
@Craig Johnston:请参考http://blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx:“如果一个本地变量没有被使用,那么不能保证它会一直存在到作用域的结束。运行时可以自由分析代码,并确定变量在某个点之后不再被使用,因此不会将该变量保持活动状态(即不会将其视为垃圾回收的根对象)。” - Randy Levy
1
@Tuzo:没错。这就是GC.KeepAlive的作用。 - Steven Sudit
1
@Craig Johnston:是和不是。不是因为.NET运行时会为您管理并且做得很好。是因为程序员的工作不仅仅是编写可以编译的代码,而是编写可以运行的代码。有时候了解运行时在底层正在做什么会有所帮助(例如故障排除)。可以说这是一种有助于区分优秀程序员和卓越程序员的知识类型。 - Randy Levy
显示剩余3条评论

20

正如其他人所说,如果类实现了IDisposable,你肯定要调用Dispose。我对此持有非常严格的立场。有些人可能会声称在DataSet上调用Dispose是毫无意义的,因为他们分解它并发现它不执行任何有意义的操作。但是,我认为这种论点中存在着谬误。

请阅读此处一位备受尊敬的人士就此问题进行的有趣辩论。随后再阅读我的理由此处,我认为Jeffery Richter所在的阵营错误。

现在,我们来讨论是否应该将引用设置为null。答案是否定的。以下代码将说明我的观点。

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}
那么当您认为由a引用的对象可以被收集时,您认为是什么时候呢?如果您说在调用a = null之后,那么您是错的。如果您说是在Main方法完成之后,那么您也是错的。正确答案是它在调用DoSomething期间的某个时间是可以被收集的。没错,它甚至可以在引用被设置为null以前,甚至可能在调用DoSomething完成之前就可以被回收了。这是因为JIT编译器可以识别对象引用何时不再被解除引用,即使它们仍然具有根引用。

5
如果a是类中的私有成员变量,如果a没有被设置为null,GC就无法知道a是否会在某个方法中再次被使用,对吧?因此,除非整个包含该变量的类被收集,否则a不会被垃圾回收器收集,对吗? - Kevin P. Rice
6
@Kevin:正确。如果a是一个类成员,并且包含a的类仍然有根并且正在使用,则a也将存在。在这种情况下,将其设置为 null 可能是有益的。 - Brian Gideon
1
您的观点与“Dispose”的重要性相关--在没有对其进行根引用的情况下不可能调用一个对象上的“Dispose”(或任何其他非可联编方法);在使用完一个对象之后调用“Dispose”将确保在其上执行的最后一个操作的持续期间仍存在根引用。放弃所有对一个对象的引用而不调用“Dispose”可能会讽刺地导致该对象的资源偶尔过早释放。 - supercat
这个例子和解释似乎并没有明确说明永远不要将引用设置为null。我的意思是,除了Kevin的评论之外,一个在被处理后设置为null的引用似乎相当“无害”,那么有什么危害呢?我有什么遗漏吗? - dathompson
@dathompson 把它设置为 null 两次也是“良性”的,但这不是做到这样的原因。(以荒谬的方式...) - StayOnTarget
有没有任何代码分析器可以遍历代码并在未处理应该被处理的事物时发出警告? - variable

18
你不需要在C#中将对象设置为null。编译器和运行时会负责确定它们何时不再处于作用域内。
是的,你应该处理实现了IDisposable接口的对象。

2
如果您有一个长寿(甚至是静态)引用大对象的情况,您应该在使用完毕后立即将其设为null,以便可以回收它。 - Steven Sudit
13
如果您曾经“完成了它”,那么它就不应该是静态的。如果它不是静态的,但是“长期存在”,那么在您完成它之后很快就应该超出作用域。需要将引用设置为null表示代码结构存在问题。 - EMP
2
如果只是暂时使用,那么它就不应该在静态字段中存储任何原始数据。当然,你可以这样做,但正是出于这个原因,这并不是一个好的实践:你随后必须手动管理其生命周期。 - EMP
3
通过将原始数据存储在处理它的方法中的本地变量中,您可以避免此问题。该方法返回处理后的数据,您保留这些数据,但是用于存储原始数据的本地变量会在方法退出时超出范围,并自动进行垃圾回收。 - EMP
“在C#中,您永远不需要将对象设置为null” - 这是一个有点夸张的说法,你有证据来支持吗?如果我有一个懒加载/单例对象,并随后调用Dispose,那么我有可能会在已经被处理的对象上尝试使用它,因为不能保证GC在再次使用之前将字段置空。” - James
显示剩余2条评论

13
如果对象实现了 IDisposable 接口,那么是的,你应该对其进行处理。这个对象可能会挂起一些本地资源(文件句柄,操作系统对象),否则这些资源可能无法立即释放。这可能导致资源匮乏、文件锁定问题和其他一些微妙的错误,否则这些问题可以避免。
请参见MSDN上的实现Dispose方法

但是垃圾回收器不会调用Dispose()吗?如果是这样,为什么还需要调用它呢? - CJ7
除非您明确调用它,否则不能保证Dispose将被调用。此外,如果您的对象持有稀缺资源或锁定某些资源(例如文件),则应尽快释放它。等待垃圾回收器完成这项工作是次优的选择。 - Chris Schmich
12
垃圾回收器不会调用Dispose()方法。垃圾回收器可能会调用一个约定的终结器来清理资源。 - adrianm
@adrianm:不是“可能会”调用,而是“一定会”调用。 - leppie
3
@leppie:终结器不是确定性的,可能不会被调用(例如当应用程序域被卸载时)。如果您需要确定性终结,则必须实现我认为称为关键处理程序的内容。CLR对这些对象有特殊处理,以确保它们被终结(例如,预先JIT终结代码以处理低内存)。 - adrianm
@adrianm:我也发现了一些奇怪的情况,比如锁对象,在这种情况下Dispose做了Finalize不做的事情。 - Steven Sudit

12

我同意这里的普遍答案,即您应该处理而不是通常不应将变量设置为null...但我想指出的是,dispose并不主要涉及内存管理。是的,它可以帮助(有时会),但它的主要目的是为您提供确定性释放稀缺资源。

例如,如果您打开硬件端口(例如串行端口),TCP / IP套接字,以排他访问模式打开文件,甚至是数据库连接,则现在已经防止任何其他代码使用这些项,直到它们被释放。 dispose通常释放这些项目(以及GDI和其他“操作系统”句柄等,虽然有数千个可用,但整体上仍然有限)。如果您不在所有者对象上调用dipose并明确释放这些资源,然后尝试在未来打开相同的资源(或另一个程序这样做),则该打开尝试将失败,因为您未处理,未收集的对象仍然保持项目的打开状态。当然,在GC收集该项时(如果正确实现了Dispose模式),资源将被释放...但您不知道那将是什么时候,因此您不知道重新打开该资源是否安全。这是Dispose解决的主要问题。当然,释放这些句柄通常也会释放内存,并且从不释放它们可能永远不会释放那些内存...因此所有关于内存泄漏或内存清理延迟的讨论。

我看到过这个问题在现实世界中引起问题。例如,我曾经见过ASP.Net Web应用程序最终无法连接到数据库(虽然仅短时间或直到Web服务器进程重新启动),因为sql server“连接池已满”...也就是说,已经创建了很多连接,并且在短时间内未显式释放,因此不能创建新连接,并且池中的许多连接虽然不活动,但仍然由未处理和未收集的对象引用,因此无法重新使用。必要时正确处理数据库连接可以确保不会发生此问题(除非您有非常高的并发访问)。


10

如果它们实现了IDisposable接口,则应该对它们进行处理。垃圾回收器将处理其余部分。

编辑:最好在使用可处理项时使用using命令:

using(var con = new SqlConnection("..")){ ...

5
当一个对象实现了 IDisposable 接口时,你应该调用 Dispose 方法(或者在某些情况下调用 Close 方法,因为它会自动调用 Dispose 方法)。
通常情况下,你不需要将对象设置为null,因为垃圾回收机制会知道对象不再被使用。
但是有一种特殊情况,当我从数据库中检索到许多需要处理的对象并将它们存储在集合(或数组)中时,当我完成这些“工作”后,我会将对象设置为 null,因为垃圾回收机制不知道我已经完成了它们的处理。
例如:
using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called

5
始终调用dispose。冒险不值得。大型托管企业应用程序应受到尊重。不能做出任何假设,否则它将回来咬你。
不要听取leppie的建议。
许多对象实际上并没有实现IDisposable,所以您不必担心它们。如果它们真正超出了范围,它们将自动释放。此外,我从未遇到过必须将某些内容设置为null的情况。
可能会发生的一件事是,许多对象可能被保持打开状态。这可能会大大增加应用程序的内存使用量。有时很难确定这是否实际上是内存泄漏,还是您的应用程序只是在做很多事情。
内存分析工具可以帮助解决这类问题,但可能会很棘手。
此外,始终取消订阅不需要的事件。同时要注意WPF绑定和控件。虽然不是常见情况,但我曾遇到这样一种情况:一个WPF控件被绑定到底层对象。底层对象很大,占用了大量内存。 WPF控件被替换为新实例,而旧实例仍然存在某种原因。这导致了内存泄漏。
回顾代码,代码写得很糟糕,但要确保未使用的内容超出范围。使用内存分析器找到这个问题花费了很长时间,因为很难知道内存中的内容是否有效,以及什么内容不应该存在。

4
通常情况下,不需要将字段设置为null。但我建议您始终处置未受控资源。
基于经验,我还建议您执行以下操作:
取消订阅任何不再需要的事件。
如果不再需要委托或表达式,请将其保存的任何字段设置为null。
我曾遇到过一些非常难以发现的问题,这些问题的直接结果就是没有遵循上述建议。
Dispose()方法是个很好的设置字段值的地方,但通常越早越好。
一般来说,如果存在对对象的引用,则垃圾收集器(GC)可能需要多几代才能确定该对象不再使用。在此期间,对象仍保留在内存中。
这可能不是问题,直到您发现应用程序使用的内存比预期多得多。当这种情况发生时,请连接内存分析器以查看未清理的对象。将引用其他对象的字段设置为null,并在Dispose()中清除集合,可以帮助GC确定哪些对象可以从内存中删除。GC将更快地回收已使用的内存,使您的应用程序更少占用内存,同时运行速度也更快。

1
“事件和委托”是什么意思 - 这些应该如何进行“清理”? - CJ7
@Craig - 我编辑了我的答案。希望这样能更清楚一些。 - Marnix van Valen

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