垃圾收集器是否会为我调用IDisposable.Dispose?

时间:2008-09-05 00:25:21

标签: .net dispose idisposable

.NET IDisposable Pattern 暗示如果您编写终结器并实现IDisposable,则终结器需要显式调用Dispose。 这是合乎逻辑的,而且在极少数情况下我总是会做终结器的保证。

然而,如果我这样做会发生什么:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

并且不实现终结器或任何东西。框架会为我调用Dispose方法吗?

是的,我意识到这听起来很愚蠢,而且所有的逻辑都暗示它不会,但我总是有两件让我不确定的东西。

  1. 几年前有人告诉过我,事实上这样做了,那个人有“非常了解他们的东西”的记录。

  2. 编译器/框架会根据您实现的接口执行其他“神奇”操作(例如:foreach,扩展方法,基于属性的序列化等),因此这也可能是“魔术” 。

  3. 虽然我已经阅读了很多有关它的内容,并且有许多隐含的内容,但我从来没有找到明确的是或否答案。

9 个答案:

答案 0 :(得分:109)

.Net垃圾收集器在垃圾收集上调用对象的Object.Finalize方法。通过默认,这会,如果您想释放其他资源,则必须覆盖。

如果要释放资源,则不会自动调用Dispose并且必须明确,例如在'using'或'try finally'块中

有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx

答案 1 :(得分:61)

我想强调Brian在评论中的观点,因为它很重要。

终结器不是像C ++那样的确定性析构函数。正如其他人所指出的那样,无法保证何时会被调用,实际上如果你有足够的内存,如果永远被调用。

但是关于终结器的坏处是,正如Brian所说,它会使你的对象在垃圾收集中存活下来。这可能很糟糕。为什么呢?

正如您可能知道或不知道的那样,GC分为几代 - 第0代,第1代和第2代,以及大对象堆。 Split是一个松散的术语 - 你得到一块内存,但是有一些指向Gen 0对象的起点和终点。

思考过程是你可能会使用很多短暂的物体。因此,对于GC来说,这些应该是简单快速的 - Gen 0对象。因此,当存在内存压力时,它首先做的是Gen 0集合。

现在,如果这没有解决足够的压力,那么它会返回并进行第1代扫描(重做第0代),然后如果仍然不够,则执行第2代扫描(重做第1代和第0代) )。因此,清理长寿命对象可能需要一段时间并且相当昂贵(因为在操作期间您的线程可能会被暂停)。

这意味着,如果您执行以下操作:

~MyClass() { }

你的对象,无论如何,都会活到第2代。这是因为GC在垃圾收集过程中无法调用终结器。所以必须最终确定的对象被移动到一个特殊的队列,由另一个线程清理掉(终结器线程 - 如果你杀了它就会发生各种坏事)。这意味着您的对象会更长时间地挂起,并可能会强制进行更多垃圾收集。

所以,所有这一切只是为了让你想要使用IDisposable来尽可能地清理资源并且认真地试图找到使用终结器的方法。这符合您的应用程序的最佳利益。

答案 2 :(得分:30)

这里已经有很多很好的讨论了,我参加派对的时间有点晚了,但我想自己补充几点。

  • 垃圾收集器永远不会直接为您执行Dispose方法。
  • GC 执行终结器。
  • 用于具有终结器的对象的一种常见模式是让它调用一个方法,该方法按惯例定义为Dispose(bool disposing)传递false以指示调用是由于终结而不是显式Dispose调用
  • 这是因为在最终确定对象时对其他托管对象做出任何假设是不安全的(它们可能已经完成)。

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

这是简单的版本,但是有很多细微差别可以让你在这种模式上绊倒。

  • IDisposable.Dispose的合同表明多次调用一定是安全的(在已经处置的对象上调用Dispose应该什么都不做)
  • 正确管理一次性对象的继承层次结构会变得非常复杂,特别是如果不同的层引入新的Disposable和非托管资源。在上面的模式中,Dispose(bool)是虚拟的,允许它被覆盖,以便可以对其进行管理,但我发现它容易出错。

在我看来,完全避免使用任何直接包含可能需要最终确定的一次性引用和本机资源的类型要好得多。 SafeHandles提供了一种非常干净的方法,通过将本机资源封装到一次性内部提供自己的最终化(以及在P / Invoke中删除窗口,其中本机句柄可能由于异步异常而丢失)等许多其他好处

简单地定义一个SafeHandle使得这个简单:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

允许您将包含类型简化为:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

答案 3 :(得分:6)

我不这么认为。您可以控制何时调用Dispose,这意味着您可以在理论上编写处理代码,以对(例如)其他对象的存在进行假设。您无法控制何时调用终结器,因此让终结器代表您自动调用Dispose将是不确定的。


编辑:我离开并进行测试,以确保:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

答案 4 :(得分:3)

不是你描述的情况, 但是GC会为你调用 Finalizer ,如果你有的话。

但是。下一个垃圾收集,而不是被收集,对象将进入终结队列,收集所有内容,然后调用终结器。之后的下一个集合将被释放。

根据应用程序的内存压力,您可能暂时没有该对象生成的gc。因此,对于文件流或数据库连接,您可能需要等待一段时间才能在终结器调用中释放非托管资源一段时间,从而导致一些问题。

答案 5 :(得分:1)

不,它没有被召唤。

但是这很容易忘记处理你的对象。只需使用using关键字。

我为此做了以下测试:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }

答案 6 :(得分:1)

GC将调用dispose。 可以调用你的终结者,但即使这样也不能保证在所有情况下。

有关处理此问题的最佳方法的讨论,请参阅此article

答案 7 :(得分:0)

IDisposable上的文档对行为以及示例代码进行了非常明确和详细的解释。 GC不会在界面上调用Dispose()方法,但它会为您的对象调用终结器。

答案 8 :(得分:0)

IDisposable模式的创建主要是由开发人员调用的,如果你有一个实现IDispose的对象,那么开发人员应该在对象的上下文周围实现using关键字,或者直接调用Dispose方法。 / p>

模式的失败保护是实现调用Dispose()方法的终结器。如果你不这样做,你可能会创建一些内存泄漏,即:如果你创建一些COM包装器,从不调用System.Runtime.Interop.Marshall.ReleaseComObject(comObject)(它将放在Dispose方法中)。

clr中没有魔法可以自动调用Dispose方法,除了跟踪包含终结器的对象并由GC将它们存储在Finalizer表中,并在GC启动一些清理启发式时调用它们。