您是否需要处置对象并将其设置为null?


310

您是否需要处置对象并将其设置为null,还是当垃圾回收器超出范围时清理它们?


4
似乎已经达成共识,您不需要将对象设置为null,但是是否需要执行Dispose()?
CJ7'5


9
如果对象实现IDisposable,我的建议是始终处以处置。每次使用using块。不要做假设,不要任凭机会。不过,您不必将其设置为null。一个对象刚超出范围。
彼得

11
@peter:不要对WCF客户端代理使用“使用”块:msdn.microsoft.com/en-us/library/aa355056.aspx
nlawalker

9
但是,您可能希望在方法中将一些引用设置为null Dispose()!这是这个问题上的一个细微变化,但很重要,因为被处置的对象无法知道它是否“超出范围”(调用Dispose()不能保证)。此处的更多信息:stackoverflow.com/questions/6757048/…–
凯文·莱斯

Answers:


239

当不再使用对象以及垃圾收集器认为合适时,它们将被清除。有时,您可能需要将对象设置为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
如果obj是引用类型,则finally块等效于:if (obj != null) ((IDisposable)obj).Dispose();
Randy支持Monica 2010年

1
@Tuzo:谢谢!编辑以反映这一点。
扎克·约翰逊

2
关于的一句话IDisposable。不处理对象通常不会在任何设计良好的类上引起内存泄漏。在C#中使用非托管资源时,您应该有一个终结器,该终结器仍将释放非托管资源。这意味着,应该在垃圾回收器最终确定托管对象时才将其推迟到应该完成的时候分配资源。但是,它仍然可能导致许多其他问题(例如未释放的锁)。你应该IDisposable尽管处置!
Aidiakapi 2015年

@RandyLevy你有参考吗?谢谢
基本的

但是我的问题是Dispose()需要实现任何逻辑吗?它需要做些什么吗?还是在内部,当Dispose()调用时发出信号GC很好?例如,我检查了TextWriter的源代码,而Dispose没有实现。
Mihail Georgescu

137

对象永远不会像在C ++中那样超出C#的范围。当它们不再使用时,垃圾收集器将自动处理它们。这比C ++更复杂,因为C ++中变量的范围完全是确定性的。CLR垃圾收集器会主动浏览所有已创建的对象,并计算出是否正在使用它们。

一个对象可以在一个函数中“超出范围”,但是如果返回了它的值,则GC将查看调用函数是否保留返回值。

将对象引用设置为 null无需因为垃圾回收通过确定其他对象正在引用哪些对象来进行工作。

在实践中,您不必担心破坏,它可以正常工作,而且很棒:)

DisposeIDisposable完成使用它们时,必须在实现的所有对象上调用它们。通常,您将对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#对象与使用newC ++语法创建的对象很像-它们是在堆上创建的,但与C ++对象不同,它们是由运行时管理的,因此您不必担心破坏它们。

由于对象始终在堆上,因此对象引用(即指针)超出范围的事实变得毫无意义。确定对象是否要收集涉及的因素比仅存在对该对象的引用要多。

C#对象引用

Jon Skeet 比较了Java中的对象引用附加到气球(即对象)上的字符串进行了比较。同样的类比适用于C#对象引用。它们只是指向包含对象的堆的位置。因此,将其设置为null不会立即影响对象的寿命,气球会继续存在,直到GC“弹出”它。

继续对气球进行类比,似乎合理的是,一旦气球没有附加任何绳子,就可以销毁它。实际上,这正是引用计数对象在非托管语言中的工作方式。除非这种方法不适用于循环引用。想象两个气球用绳子连接在一起,但是两个气球都没有绳子。在简单的引用计数规则下,即使整个气球组是“孤立的”,它们也都继续存在。

.NET对象非常像屋顶下的氦气球。当车顶打开(GC运行)时,即使可能有成组的气球捆绑在一起,未使用的气球也会飘走。

.NET GC将世代GC与标记和清除结合使用。分代方法涉及运行时,它倾向于检查最近分配的对象,因为它们很可能未被使用,而标记和清除则涉及运行时遍历整个对象图,并确定是否存在未使用的对象组。这足以处理循环依赖问题。

另外,.NET GC在另一个线程(所谓的终结器线程)上运行,因为它有很多工作要做,而在主线程上执行该操作将中断您的程序。


1
@Igor:“超出范围”是指对象引用超出上下文并且不能在当前范围中引用。当然,这在C#中仍然会发生。
CJ7'5

@Craig Johnston,不要将编译器使用的变量作用域与变量生存期(由运行时确定)混淆,它们是不同的。即使局部变量仍在范围内,它也可能不是“活动的”。
兰迪(Randy)支持莫妮卡(Monica)2010年

1
@克雷格·约翰斯顿(Craig Johnston):请参阅blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx:“无法保证局部变量在没有作用域的情况下一直有效,直到作用域结束运行时可以自由地分析其所拥有的代码,并确定在某个特定点之外没有进一步使用该变量的情况,因此可以使该变量不超过该特定点(即,不将其视为根目的) GC)。”
兰迪(Randy)

1
@Tuzo:是的。那就是GC.KeepAlive的目的。
Steven Sudit 2010年

1
@克雷格·约翰斯顿:不,是的。否,因为.NET运行时可以为您管理该工作并且做得很好。是的,因为程序员的工作不是写(只是)编译的代码而是写运行的代码。有时,它有助于了解运行时的内容(例如,故障排除)。有人可能会说,正是这种知识类型有助于将优秀的程序员与优秀的程序员区分开。
兰迪(Randy)

18

正如其他人所说,Dispose如果该类实现,则您肯定要调用IDisposable。我对此持严格的立场。例如,有些人可能认为Disposeon的调用DataSet是没有意义的,因为他们对其进行了分解并发现它没有做任何有意义的事情。但是,我认为这种说法存在很多谬论。

阅读此文章,以备受尊敬的个人对此话题进行有趣的辩论。然后在这里阅读我的推理为什么我认为杰弗里·里希特(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编译器可以识别何时不再取消引用对象引用,即使它们仍然是根的。


3
如果a班上有一个私人成员字段怎么办?如果a未将其设置为null,则GC无法确定是否a将以某种方法再次使用它,对吗?因此,a直到收集了整个包含类,才将其收集。没有?
凯文·P·赖斯

4
@凯文:对。如果a是一个班级成员,并且包含的​​班级a仍然植根并在使用中,那么它也将继续存在。这是将其设置null为有益的一种情况。
Brian Gideon

1
您的观点与Dispose重要的原因联系在一起- Dispose在没有根引用的情况下无法在对象上调用(或任何其他不可内联的方法);Dispose使用一个对象完成调用后,将确保在该对象上执行的最后一个操作的整个过程中,有根引用将继续存在。Dispose具有讽刺意味的是,放弃对对象的所有引用可能会导致对象资源偶尔被过早释放。
2015年

对于从不将引用设置为null的硬性建议,此示例和解释似乎并不明确。我的意思是,除了Kevin的评论外,引用被处置后设置为null 似乎是无害的,那有什么害处?我想念什么吗?
dathompson

13

您无需在C#中将对象设置为null。编译器和运行时将负责确定它们何时不在范围内。

是的,您应该处置实现IDisposable的对象。


2
如果您有对大型对象的长期(甚至是静态)引用,want则在完成使用后立即将其清空,以便可以免费回收它。
Steven Sudit 2010年

12
如果您曾经“完成”,那么它就不应是静态的。如果它不是静态的,而是“长寿的”,那么在使用完它之后,它应该仍然会超出范围。需要将引用设置为null表示代码结构有问题。
EMP 2010年

您可以完成一个静态项目。考虑:一种静态资源,它以用户友好的格式从磁盘读取,然后解析为适合程序使用的格式。您最终可能会获得原始数据的私有副本,此副本不再具有任何用途。(真实世界的示例:解析是两次通过的例程,因此不能简单地在读取数据时对其进行处理。)
Loren Pechtel 2010年

1
然后,如果它只是临时使用,则不应在静态字段中存储任何原始数据。当然,您可以这样做,由于这个原因,这不是一个好习惯:您必须手动管理其生命周期。
EMP 2010年

2
通过将原始数据存储在处理它的方法中的局部变量中,可以避免这种情况。该方法返回保留的已处理数据,但是当该方法退出并自动GC时,原始数据的本地数据超出范围。
EMP 2010年

11

我同意这里的常见答案,是的,应该处置,不,通常不应将变量设置为null ...,但我想指出的是,处置并非主要与内存管理有关。是的,它可以帮助(有时也可以)帮助进行内存管理,但是其主要目的是为您确定性地释放稀缺资源。

例如,如果您打开一个硬件端口(例如串行),一个TCP / IP套接字,一个文件(处于独占访问模式)或什至是一个数据库连接,现在您将阻止任何其他代码在发布这些项目之前使用这些项目。处理通常会释放这些项目(连同GDI和其他“ os”句柄等,它们有1000个可用,但总体上还是有限的)。如果您不对所有者对象调用dipose并显式释放这些资源,则以后尝试再次打开同一资源(或另一个程序这样做),则打开尝试将失败,因为您未处置,未收集的对象仍具有打开的项目。当然,当GC收集项目时(如果Dispose模式已正确实现),资源将被释放...但是您不知道何时发布,因此您不会 不知道何时可以安全地重新打开该资源。这是Dispose解决的主要问题。当然,释放这些句柄也常常会释放内存,从不释放它们可能永远也不会释放该内存...因此,所有有关内存泄漏或内存清理延迟的讨论。

我已经看到了导致问题的现实例子。例如,我已经看到ASP.Net Web应用程序最终无法连接到数据库(尽管时间很短,或者直到Web服务器进程重新启动),因为sql服务器的“连接池已满” ...即,如此之多的连接已经创建并且在很短的时间内没有被明确释放,以至于无法创建新的连接,并且池中的许多连接(虽然不是活动的)仍然被未分配和未收集的对象引用,因此可以”不能重复使用。正确处理与数据库的连接在必要确保这一问题不会发生(至少不会,除非你有非常高的并发访问)。


11

如果该对象实现IDisposable,则可以,请对其进行处理。该对象可能挂在本机资源(文件句柄,OS对象)上,否则本机资源可能不会立即释放。这可能导致资源匮乏,文件锁定问题以及其他本可以避免的细微错误。

另请参见在MSDN上实现处置方法


但是垃圾回收器不会调用Dispose()吗?如果是这样,您为什么需要调用它?
CJ7

除非您明确地调用它,否则不能保证Dispose会被调用。另外,如果您的对象正在使用稀缺资源或正在锁定某些资源(例如文件),那么您将希望尽快释放它。等待GC这样做不是最佳的。
克里斯·施密克

12
GC将永远不会调用Dispose()。GC可以调用终结器,按照惯例,终结器应该清理资源。
阿德里安

@adrianm:不是might打电话,而是will打电话。
嬉皮

2
@leppie:终结器不是确定性的,可能无法调用(例如,当卸载appdomain时)。如果需要确定性的终结处理,则必须实现我认为的关键处理程序。CLR对这些对象进行特殊处理以确保它们已完成(例如,它预先敲入了完成代码以处理低内存)
adrianm 2010年

9

如果它们实现IDisposable接口,则应将其处置。垃圾收集器将负责其余的工作。

编辑:最好是using在处理一次性物品时使用命令:

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

5

当对象实现时,IDisposable您应调用Dispose(或Close,在某些情况下,它将为您调用Dispose)。

您通常不必将对象设置为 null,因为GC将知道不再使用对象。

将对象设置为时,有一个例外null。当我从数据库中检索许多需要处理的对象并将它们存储在集合(或数组)中时。完成“工作”后,我将对象设置为null,因为GC不知道我已经完成了工作。

例:

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

4

通常,无需将字段设置为null。我总是建议您处置非托管资源。

根据经验,我还建议您执行以下操作:

  • 如果您不再需要事件,请取消订阅。
  • 将不再包含委托或表达式的任何字段设置为null。

我遇到了一些很难找到的问题,这些问题是不遵循上述建议的直接结果。

一个合适的地方是在Dispose()中,但是通常越早越好。

通常,如果存在对对象的引用,则垃圾收集器(GC)可能需要花费几代时间才能确定不再使用该对象。对象始终保留在内存中。

在您发现应用程序使用的内存比您预期的要多得多之前,这可能不是问题。发生这种情况时,请连接内存分析器以查看未清除的对象。将引用其他对象的字段设置为null并清除处置集合,可以真正帮助GC确定可以从内存中删除哪些对象。GC可以更快地回收使用的内存,从而使您的应用程序少很多内存且更快。


1
您对“事件和代表”的含义是什么-应该用这些“清理”什么?
CJ7'5

@Craig-我编辑了答案。希望这可以澄清一下。
Marnix van Valen 2010年

3

总是打电话处理。不值得冒险。大型托管企业应用程序应得到尊重。无法做任何假设,否则它会再次咬你。

不要听嬉皮士。

许多对象实际上并没有实现IDisposable,因此您不必担心它们。如果它们确实超出范围,它们将自动释放。另外,我从未遇到过必须将某些内容设置为null的情况。

可能发生的一件事是许多对象可以保持打开状态。这会大大增加应用程序的内存使用率。有时很难弄清楚这是否实际上是内存泄漏,或者您的应用程序是否正在做很多事情。

内存配置文件工具可以帮助解决类似问题,但这可能很棘手。

此外,请始终取消订阅不需要的事件。还应注意WPF绑定和控件。这不是通常的情况,但是我遇到了一个WPF控件绑定到基础对象的情况。基础对象很大,并且占用了大量内存。WPF控件已替换为新实例,而旧实例由于某种原因仍在徘徊。这导致大量内存泄漏。

后面的代码编写得很差,但是关键是要确保不使用的内容超出范围。使用内存分析器花了很长时间才找到它,因为很难知道内存中的什么是有效的,什么不应该存在。


2

我也要回答 JIT从对变量用法的静态分析中生成表以及代码。这些表条目是当前堆栈帧中的“ GC根”。随着指令指针的前进,这些表条目变得无效,因此可以进行垃圾回收。因此:如果是作用域变量,则无需将其设置为null-GC将收集对象。如果它是成员或静态变量,则必须将其设置为null

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.