您是否需要处理对象并将其设置为null,或者当它们超出作用域时,垃圾收集器将清理它们?


当前回答

如果对象实现了IDisposable,那么是的,你应该释放它。对象可能挂在本地资源(文件句柄、操作系统对象)上,否则这些资源可能不会立即释放。这可能导致资源短缺、文件锁定问题和其他本来可以避免的微妙错误。

参见在MSDN上实现一个Dispose方法。

其他回答

通常,不需要将字段设置为null。然而,我总是建议丢弃非托管资源。

根据我的经验,我还建议你这样做:

如果不再需要,则取消订阅事件。 如果不再需要,将持有委托或表达式的任何字段设置为空。

我遇到过一些很难发现的问题,这些问题都是由于没有遵循上述建议而直接导致的。

Dispose()是执行此操作的一个好地方,但通常越快越好。

一般来说,如果存在对某个对象的引用,垃圾收集器(GC)可能要多花几代时间才能确定该对象不再使用。在此期间,对象始终保留在内存中。

这可能不是一个问题,直到你发现你的应用程序正在使用比你预期的更多的内存。当发生这种情况时,连接内存分析器来查看哪些对象没有被清理。将引用其他对象的字段设置为null并在处理时清除集合可以真正帮助GC确定可以从内存中删除哪些对象。GC将更快地回收使用的内存,使您的应用程序的内存需求更少,更快。

正如其他人所说,如果类实现了IDisposable,则肯定需要调用Dispose。我在这个问题上的立场相当坚定。例如,有些人可能会声称在DataSet上调用Dispose是没有意义的,因为他们分解了DataSet,发现它没有做任何有意义的事情。但是,我认为这种说法有很多谬误。

阅读这篇文章,你会看到一场由受人尊敬的人就这个问题展开的有趣辩论。然后阅读我的推理,为什么我认为杰弗里·里希特站在错误的阵营。

现在,关于是否应该将引用设置为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");
}

So when do you think the object referenced by a is eligible for collection? If you said after the call to a = null then you are wrong. If you said after the Main method completes then you are also wrong. The correct answer is that it is eligible for collection sometime during the call to DoSomething. That is right. It is eligible before the reference is set to null and perhaps even before the call to DoSomething completes. That is because the JIT compiler can recognize when object references are no longer dereferenced even if they are still rooted.

我也得回答。 JIT从对变量使用情况的静态分析中生成表和代码。 这些表项是当前堆栈帧中的“GC-Roots”。随着指令指针的前进,这些表项变得无效,因此准备进行垃圾收集。 因此:如果它是一个作用域变量,你不需要将它设置为null - GC将收集该对象。 如果它是一个成员或静态变量,则必须将其设置为null

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

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

当我将对象设置为空时,有一个例外。当我(从数据库中)检索了很多需要处理的对象,并将它们存储在一个集合(或数组)中。当“工作”完成时,我将对象设置为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

在c#中,对象永远不会像在c++中那样超出范围。当它们不再被使用时,垃圾回收器会自动处理它们。这是一种比c++更复杂的方法,在c++中,变量的作用域是完全确定的。CLR垃圾收集器会主动遍历所有已创建的对象,并判断它们是否正在被使用。

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

将对象引用设置为空是不必要的,因为垃圾收集的工作原理是确定哪些对象正在被其他对象引用。

在实践中,你不必担心破坏,它只是工作,它是伟大的:)

当你完成对所有实现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++中使用新语法创建的对象——它们是在堆上创建的,但与c++对象不同的是,它们由运行时管理,所以你不必担心销毁它们。

由于对象总是在堆上,对象引用(即指针)超出作用域的事实变得毫无意义。在确定是否收集对象时,涉及到的因素不仅仅是对对象的引用。

c#对象引用

Jon Skeet将Java中的对象引用与连接到气球(即对象)上的字符串段进行了比较。同样的类比也适用于c#对象引用。它们只是指向包含该对象的堆的位置。因此,将它设置为null对对象的生命周期没有立即影响,气球继续存在,直到GC“弹出”它。

Continuing down the balloon analogy, it would seem logical that once the balloon has no strings attached to it, it can be destroyed. In fact this is exactly how reference counted objects work in non-managed languages. Except this approach doesn't work for circular references very well. Imagine two balloons that are attached together by a string but neither balloon has a string to anything else. Under simple ref counting rules, they both continue to exist, even though the whole balloon group is "orphaned".

. net对象很像屋顶下的氦气球。当屋顶打开(GC运行)-未使用的气球飘走,即使有可能有一组气球拴在一起。

. net GC使用分代GC和标记和清除的组合。分代方法涉及到运行时倾向于检查最近分配的对象,因为它们更有可能未使用,而标记和扫描涉及到运行时遍历整个对象图,并确定是否有未使用的对象组。这充分地解决了循环依赖问题。

此外,. net GC运行在另一个线程(所谓的终结器线程)上,因为它有相当多的事情要做,在主线程上这样做会中断你的程序。