将对象设置为null与Dispose()


108

我对CLR和GC的工作方式着迷(我正在通过C#,Jon Skeet的书/帖子等阅读CLR来扩展我的知识)。

无论如何,说之间有什么区别:

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

还是通过使MyClass实现IDisposable和析构函数并调用Dispose()来实现?

另外,如果我有一个带有using语句的代码块(例如,下面),如果我单步执行该代码并退出using块,那么该对象是否已被处置?如果我在using块中调用Dispose()会怎样呢?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

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

Answers:


210

将处理与垃圾收集分开很重要。它们是完全独立的事物,只有一点共同点,我将在稍后讨论。

Dispose,垃圾收集和完成

当您编写一条using语句时,它只是try / finally块的语法糖,因此Dispose即使using语句主体中的代码引发异常也将被调用。这并不意味着对象是在块末尾被垃圾回收的。

处置是关于非托管资源(非内存资源)的。这些可能是UI句柄,网络连接,文件句柄等。这些是有限的资源,因此您通常希望尽快释放它们。你应该执行IDisposable,只要你喜欢的类型“拥有”一个非托管资源,直接(通常通过IntPtr)或间接(例如,通过Stream中,SqlConnection等)。

垃圾回收本身仅与内存有关,只有一点点扭曲。垃圾收集器能够找到不再被引用的对象,并释放它们。但是,它不会一直在寻找垃圾-仅当它检测到需要垃圾时(例如,如果“一代”堆内存不足)。

转折是定案。垃圾收集器保留了一个不再可访问的对象列表,这些对象具有终结器(用~Foo()C#编写,有些令人困惑-它们与C ++析构函数完全不同)。它在这些对象上运行终结器,以防万一它们需要在释放内存之前进行额外的清理。

在该类型的用户忘记按顺序处理它的情况下,几乎总是使用终结器来清理资源。因此,如果您打开a FileStream但忘记调用Disposeor Close,则终结器最终将为您释放基础文件句柄。在一个写得很好的程序中,我认为终结器几乎永远不会触发。

将变量设置为 null

关于将变量设置为的一点要点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 / finalizer

那么,您自己的类型是否应该实现终结器?几乎可以肯定。如果您仅间接持有非托管资源(例如,您拥有一个FileStream作为成员变量的资源),那么添加自己的终结器将无济于事:当您的对象处于访问状态时,流几乎肯定可以进行垃圾回收,因此您可以依靠FileStream具有终结器(如有必要-可能引用其他内容,等等)。如果您想直接“几乎”持有一个非托管资源,SafeHandle那将是您的朋友-花费一些时间来进行开发,但这意味着您几乎 再也不需要编写终结器了。如果您对资源(IntPtr)拥有真正直接的句柄,并且通常应该转向SafeHandle你尽快做。(那里有两个链接-最好同时阅读。)

乔·达菲(Joe Duffy)关于终结器和IDisposable(与很多聪明的人共同编写)的准则非常长,值得一读。值得一提的是,如果密封了类,则可以使工作更加轻松:Dispose调用新的虚拟Dispose(bool)方法等的重写模式仅在您的类设计为继承时才有意义。

这有点混乱,但是请澄清您想要的地方:)


关于“一次可能值得将局部变量设置为null的时间”-也许还有一些棘手的“捕获”方案(多次捕获同一变量)-但可能不值得使帖子复杂化!+1 ...
Marc Gravell

@Marc:是的,我什至没有想到捕获的变量。嗯 是的,我想我会把它留在那儿;)
乔恩·斯凯特

您能否告诉我们,在上面的代码段中设置“ foo = null”会发生什么?据我所知,该行仅清除指向托管堆中foo对象的变量的值?所以问题是那里的foo对象会发生什么?我们不应该称之为处理吗?
odiseh 2010年

@odiseh:如果物体是一次性的,那么可以-您应该将其丢弃。答案的这一部分仅涉及垃圾收集,这是完全独立的。
乔恩·斯基特

1
我一直在寻找有关IDisposable问题的澄清说明,因此我在Google上搜索了“ IDisposable Skeet”并找到了它。大!:D
Maciej Wozniak

22

处置对象时,资源将被释放。当您为变量分配null时,您只是在更改引用。

myclass = null;

执行完此操作后,myclass所引用的对象仍然存在,并且将一直存在,直到GC清理它为止。如果Dispose被显式调用,或者位于using块中,则将尽快释放所有资源。


7
在执行该行之后,它可能仍然不存在-它可能已该行之前被垃圾回收。JIT很聪明-像这样的线几乎总是无关紧要的。
乔恩·斯基特

6
设置为null可能意味着该对象所拥有的资源永远不会被释放。GC不会处理,它只会完成处理,因此,如果对象直接持有非托管资源,并且其终结处理程序未处理(或没有终结处理程序),则这些资源将泄漏。有一点要注意。
路加福音

6

这两个操作之间没有多大关系。当您将引用设置为null时,它只是这样做。它本身根本不影响所引用的类。您的变量不再只是指向它曾经使用过的对象,而是对象本身未更改。

当您调用Dispose()时,它是对对象本身的方法调用。现在,对对象执行Dispose方法的任何操作。但这不会影响您对对象的引用。

重叠的唯一方面是,不再有对对象的引用时,最终将收集垃圾。并且,如果该类实现了IDisposable接口,则在对象被垃圾回收之前,将在对象上调用Dispose()。

但这不会在您将引用设置为null之后立即发生,原因有两个。首先,可能存在其他引用,因此它根本不会收集垃圾,其次,即使那是最后一个引用,所以现在可以进行垃圾收集了,除非垃圾收集器决定删除,否则什么也不会发生物体。

在对象上调用Dispose()不会以任何方式“杀死”该对象。它通常被用来使清理对象可以被安全地删除之后,但最终,有关于处置什么神奇,它只是一个类的方法。


我认为这个答案是对“递归”答案的补充或详述。
dance2die
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.