将一个对象设置为null和Dispose()的区别

116

我对CLR和GC的工作原理非常着迷(我正在通过阅读《CLR via C#》、Jon Skeet的书籍/文章等来扩展我的知识)。

无论如何,说“:

MyClass myclass = new MyClass();
myclass = null;

那么,是通过让MyClass实现IDisposable和一个析构函数,并调用Dispose()吗?

另外,如果我有一个带有using语句的代码块(如下所示),如果我逐步执行代码并退出using块,那么对象会在那时被处理吗,还是等到垃圾收集发生时才会处理?如果我在using块中调用Dispose()会发生什么?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

流类(例如BinaryWriter)有Finalize方法?我为什么要使用它?

3个回答

230
重要的是将处理和垃圾收集分开。它们是完全不同的东西,只有一个共同点,我马上就会谈到。
当您编写using语句时,它只是try/finally块的语法糖,以便在using语句主体中的代码引发异常时仍调用Dispose。这并不意味着对象在块结束时被垃圾收集。
处理是关于非托管资源(非内存资源)。这些可以是UI句柄、网络连接、文件句柄等有限资源,因此通常希望尽早释放它们。每当类型“拥有”非托管资源时(通常通过IntPtr直接或间接地(例如通过Stream、SqlConnection等)),应实现IDisposable。
垃圾收集本身仅涉及内存,有一个小变化。垃圾回收器能够找到不能再引用的对象并释放它们。但它并不总是查找垃圾 - 只有在检测到需要(例如,如果堆的一个“代”用尽了内存)时才进行查找。
变化是finalization。垃圾回收器保留了一个不再可达但具有终结器(在C#中写为~Foo(),有点令人困惑 - 它们与C++析构函数完全不同)的对象列表。它运行这些对象的终结器,以防它们在释放内存之前需要进行额外的清理。
几乎总是使用终结器来清除资源,以防用户在有序方式下忘记处理它。因此,如果您打开FileStream但忘记调用Dispose或Close,则最终器将为您“最终”释放底层文件句柄。在编写良好的程序中,我的意见是几乎永远不应触发终结器。
关于将变量设置为null的一个小点 - 几乎从不需要为了垃圾收集而这样做。如果它是成员变量,您可能有时想这样做,尽管根据我的经验,“对象”的“部分”很少再次需要。当它是局部变量时,JIT通常足够聪明(在发布模式下)以知道何时不再使用引用。例如:
StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

只有在循环中,当您知道某些分支不再需要使用变量时,可能值得将本地变量设置为null。例如:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

实现IDisposable/finalizers
那么,你自己的类型是否应该实现finalizers呢?几乎肯定不需要。如果你只是间接持有非托管资源(例如,你有一个FileStream作为成员变量),那么添加自己的终结器是没有帮助的:当您的对象可回收时,流几乎肯定也可以进行垃圾回收,因此您可以依赖于FileStream具有终结器(如果必要-它可能引用其他内容等)。如果您想“几乎”直接拥有非托管资源,则SafeHandle是您的朋友-它需要一些时间才能启动,但这意味着您将几乎永远不需要再编写终结器。通常只有在您对资源拥有真正直接的处理句柄(IntPtr)时才需要终结器,并且您应该尽快转移到SafeHandle。 (有两个链接-最好都阅读。)
Joe Duffy有一篇非常长的关于finalizers和IDisposable的指南(与许多聪明的人共同撰写),值得阅读。请注意,如果您封闭了您的类,那么生活会变得更加轻松:只有在您的类设计用于继承时,覆盖Dispose以调用新的虚拟Dispose(bool)方法等模式才相关。
这有点冗长,请在需要澄清时提出问题 :)

@Marc:没错 - 我甚至没有考虑到捕获变量。嗯,我想我会放弃这个想法 ;) - Jon Skeet
请问当您在上面的代码片段中设置“foo=null”时会发生什么? 据我所知,这行代码只清除了指向托管堆中foo对象的变量的值?那么问题是foo对象会发生什么?难道我们不应该调用dispose吗? - odiseh
如果该对象是可处理的,那么是的 - 你应该处理它。但是,答案中的那一部分只涉及垃圾回收,这是完全不同的事情。 - Jon Skeet
特别要注意的是:如果您封闭了您的类,那么生活会变得更加轻松:覆盖Dispose方法以调用新的虚拟Dispose(bool)方法等模式仅适用于设计为继承的类。 - Jeff
1
我正在寻找有关一些IDisposable问题的澄清,所以我在谷歌上搜索了“IDisposable Skeet”,然后发现了这个。太好了!:D - Maciej Wozniak
显示剩余2条评论

22

当您处理一个对象时,资源将被释放。 当您将null赋值给一个变量时,您只是改变了引用。

myclass = null;

执行完此操作后,myclass对象仍然存在,只有在GC清理它之前才会消失。如果显式调用Dispose或使用using块,则任何资源将尽快被释放。


7
在执行那行代码之后,它可能已经被垃圾回收器在那行代码之前清除了,因此它可能不再存在。JIT非常智能,这使得这样的代码几乎总是无关紧要的。 - Jon Skeet
7
将对象设置为空可能意味着该对象持有的资源永远不会被释放。垃圾回收器(GC)仅执行终结操作,而不是清理操作,因此如果对象直接持有非托管资源且其终结器没有进行清理(或者它没有终结器),那么这些资源将会泄露。需要注意这一点。 - LukeH

9
这两个操作没有太多关联。当你将引用设置为null时,它只是简单地这样做。这本身不会影响到原来被引用的类。你的变量只是不再指向它以前指向的对象,但对象本身并没有改变。
当你调用Dispose()时,它是在对象本身上进行的方法调用。无论Dispose方法做什么,现在都会在对象上执行。但这不会影响你对该对象的引用。
唯一的交叉点是,当没有更多引用指向一个对象时,它最终会被垃圾回收。如果该类实现了IDisposable接口,则在垃圾回收之前将在对象上调用Dispose()。
但这并不会立即发生在你将引用设置为null之后,有两个原因。首先,可能存在其他引用,因此它根本不会被垃圾回收,其次,即使那是最后一个引用,所以现在准备好被垃圾回收了,也要等到垃圾收集器决定删除对象之前才会发生任何事情。
在对象上调用Dispose()不会以任何方式“杀死”该对象。通常用于清理,以便可以安全地删除对象,但归根结底,Dispose没有任何神奇之处,它只是一个类方法。

1
我认为这个答案是对“递归”答案的补充或细节。 - dance2die
如何查看内存使用情况?先生,如果我有很多MB的数据并想要调试这个语句,是否有任何方法来证明这个条件是真实的还是不真实的? - toha

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