通过阅读Microsoft文档,我知道IDisposable接口的“主要”用途是清理非托管资源。

对我来说,“非托管”意味着数据库连接、套接字、窗口句柄等。但是,我看到过一些代码,其中使用Dispose()方法来释放托管资源,这对我来说似乎是多余的,因为垃圾收集器应该为你负责。

例如:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }
}

我的问题是,这是否会使MyCollection使用的垃圾收集器释放内存比正常情况更快?


编辑:到目前为止,人们已经发布了一些使用IDisposable清理非托管资源(如数据库连接和位图)的好例子。但假设上述代码中的_theList包含一百万个字符串,并且您希望现在释放内存,而不是等待垃圾收集器。上面的代码能做到这一点吗?


当前回答

在您发布的示例中,它仍然没有“立即释放内存”。所有内存都是垃圾收集的,但它可能允许在早期版本中收集内存。你必须运行一些测试才能确定。


框架设计指南是指南,而不是规则。它们告诉你界面主要用于什么,何时使用,如何使用,以及何时不使用。

我曾经读过一段代码,它是一个利用IDisposable失败时的简单RollBack()。下面的MiniTx类将检查Dispose()上的一个标志,如果Commit调用从未发生,它将调用回滚。它添加了一层间接层,使调用代码更易于理解和维护。结果如下:

using( MiniTx tx = new MiniTx() )
{
    // code that might not work.

    tx.Commit();
} 

我也看到计时/日志代码做了同样的事情。在这种情况下,Dispose()方法停止计时器并记录块已退出。

using( LogTimer log = new LogTimer("MyCategory", "Some message") )
{
    // code to time...
}

因此,这里有几个具体的示例,它们不执行任何非托管资源清理,但成功地使用IDisposable创建了更干净的代码。

其他回答

IDisposable适用于取消订阅事件。

是的,这段代码完全是多余和不必要的,它不会让垃圾回收器做任何它不会做的事情(即,一旦MyCollection的实例超出范围),尤其是.Clear()调用。

对编辑的回答:有点。如果我这样做:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has no Dispose() method
    instance.FillItWithAMillionStrings();
}

// 1 million strings are in memory, but marked for reclamation by the GC

在内存管理方面,它的功能与此相同:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has your Dispose()
    instance.FillItWithAMillionStrings();
    instance.Dispose();
}

// 1 million strings are in memory, but marked for reclamation by the GC

如果您真的真的需要立即释放内存,请调用GC.Collect()。内存将在需要时释放。

如果MyCollection无论如何都将被垃圾收集,那么您不需要对其进行处理。这样做只会过度消耗CPU,甚至可能使垃圾收集器已经执行的一些预先计算的分析无效。

我使用IDisposable来执行诸如确保线程以及非托管资源被正确处理之类的操作。

编辑回应斯科特的评论:

GC性能指标唯一受到影响的时间是调用〔sic〕GC.Collect()时“

从概念上讲,GC维护对象引用图的视图,以及线程堆栈帧中对它的所有引用。这个堆可能非常大,并跨越许多页面的内存。作为优化,GC缓存对不太可能经常更改的页面的分析,以避免不必要地重新扫描页面。当页面中的数据发生更改时,GC从内核接收通知,因此它知道页面已脏,需要重新扫描。如果集合在Gen0中,那么页面中的其他内容也可能发生变化,但在Gen1和Gen2中这种情况发生的可能性较小。有趣的是,对于将GC移植到Mac以使Silverlight插件在该平台上工作的团队来说,这些钩子在Mac OS X中不可用。

反对不必要的资源处置的另一点是:想象一个进程正在卸载的情况。想象一下,这个过程已经运行了一段时间。很可能该进程的许多内存页已被交换到磁盘。至少它们不再位于L1或L2缓存中。在这种情况下,正在卸载的应用程序没有必要将所有数据和代码页交换回内存,以“释放”进程终止时操作系统无论如何都会释放的资源。这适用于托管资源,甚至某些非托管资源。只有使非后台线程保持活动状态的资源才能被释放,否则进程将保持活动状态。

现在,在正常执行期间,必须正确清理临时资源(正如@fezmonkey指出的数据库连接、套接字和窗口句柄),以避免非托管内存泄漏。这些都是必须处理的事情。如果您创建了一个拥有线程的类(我的意思是它创建了线程,因此负责确保它停止,至少按照我的编码风格),那么该类很可能必须实现IDisposable,并在Dispose期间拆除线程。

.NET框架使用IDisposable接口作为开发人员必须释放此类的信号,甚至是警告。我想不出框架中实现IDisposable的任何类型(不包括显式接口实现),其中处置是可选的。

处置托管资源最合理的用例是为GC回收资源做准备,否则这些资源将永远不会被收集。

一个主要的例子是循环引用。

虽然使用避免循环引用的模式是最佳做法,但如果您最终(例如)得到了一个“子”对象,该对象的引用返回到其“父”,那么如果您只是放弃引用并依赖GC,则这可能会停止父对象的GC收集,而且如果您实现了终结器,则永远不会调用它。

解决此问题的唯一方法是通过将子级上的Parent引用设置为null来手动打破循环引用。

在父母和孩子身上实现IDisposable是实现这一点的最佳方式。当对父级调用Dispose时,对所有子级调用Disposit,并在子级Dispose方法中,将父级引用设置为null。

Dispose的目的是释放非托管资源。这需要在某个时候完成,否则它们永远不会被清理。垃圾回收器不知道如何对IntPtr类型的变量调用DeleteHandle(),也不知道是否需要调用DeleteHandler()。

注意:什么是非托管资源?如果您在Microsoft.NET Framework中找到它:它是托管的。如果你自己去打听MSDN,它是不受管理的。任何您使用P/Invoke调用来脱离.NET Framework中所有可用内容的舒适世界的东西都是非托管的,现在您负责清理它。

您创建的对象需要公开一些外部世界可以调用的方法,以便清理非托管资源。该方法可以任意命名:

public void Cleanup()

or

public void Shutdown()

但是,这种方法有一个标准化的名称:

public void Dispose()

甚至还创建了一个接口IDisposable,它只有一个方法:

public interface IDisposable
{
   void Dispose()
}

因此,您让对象公开IDisposable接口,这样您就可以保证编写了一个方法来清理非托管资源:

public void Dispose()
{
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}

你就完了。除非你能做得更好。


如果对象分配了250MB的System.Drawing.Bitmap(即.NET托管的Bitmap类)作为某种帧缓冲区,该怎么办?当然,这是一个托管的.NET对象,垃圾收集器会释放它。但你真的想把250MB的内存留在那里,等待垃圾收集器最终来释放它吗?如果有开放的数据库连接怎么办?当然,我们不希望连接处于打开状态,等待GC完成对象。

如果用户调用了Dispose()(意味着他们不再计划使用该对象),为什么不去掉那些浪费的位图和数据库连接呢?

因此,现在我们将:

摆脱非托管资源(因为我们必须这样做),以及摆脱管理资源(因为我们想提供帮助)

因此,让我们更新Dispose()方法以消除这些托管对象:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}

一切都很好,除了你能做得更好!


如果用户忘记对对象调用Dispose(),该怎么办?然后他们会泄露一些未管理的资源!

注意:它们不会泄漏托管资源,因为垃圾收集器最终将在后台线程上运行,并释放与任何未使用对象相关联的内存。这将包括您的对象和您使用的任何托管对象(例如Bitmap和DbConnection)。

如果该人忘记调用Dispose(),我们仍然可以保存他们的培根!我们仍然有一种方法来调用它们:当垃圾收集器最终释放(即完成)我们的对象时。

注意:垃圾收集器最终将释放所有托管对象。当它完成时,它调用Finalize方法。GC不知道,或者注意Dispose方法。那只是我们选的名字当我们想要获取清除未管理的内容。

垃圾收集器对对象的破坏是释放那些讨厌的非托管资源的最佳时机。我们通过重写Finalize()方法来实现这一点。

注意:在C#中,您没有显式重写Finalize()方法。编写一个看起来像C++析构函数的方法编译器将其作为Finalize()方法的实现:

~MyObject()
{
    //we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
    Dispose(); //<--Warning: subtle bug! Keep reading!
}

但代码中有一个错误。你看,垃圾收集器在后台线程上运行;你不知道两个物体被摧毁的顺序。完全有可能在Dispose()代码中,您试图删除的托管对象(因为您希望有所帮助)不再存在:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
      this.frameBufferImage = null;
   }
}

因此,您需要的是Finalize()告诉Dispose()它不应该接触任何托管资源(因为它们可能不再存在),同时释放非托管资源。

执行此操作的标准模式是让Finalize()和Dispose()都调用第三个(!)方法;如果从Dispose()调用它(与Finalize()相反),则传递布尔值,这意味着释放托管资源是安全的。

这个内部方法可以被赋予一些任意的名称,如“CoreDispose”或“MyInternalDispose”,但传统上称它为Dispose(Boolean):

protected void Dispose(Boolean disposing)

但更有用的参数名称可能是:

protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too, but only if I'm being called from Dispose
   //(If I'm being called from Finalize then the objects might not exist
   //anymore
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}

然后将IDisposable.Dispose()方法的实现更改为:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
}

以及终结器:

~MyObject()
{
   Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}

注意:如果对象从实现Dispose的对象下降,那么在重写Dispose时不要忘记调用它们的基本Dispose方法:

public override void Dispose()
{
    try
    {
        Dispose(true); //true: safe to free managed resources
    }
    finally
    {
        base.Dispose();
    }
}

一切都很好,除了你能做得更好!


如果用户对您的对象调用Dispose(),那么一切都已清理完毕。稍后,当垃圾收集器出现并调用Finalize时,它将再次调用Dispose。

这不仅是浪费,而且如果您的对象对上次调用Dispose()时已处理的对象有垃圾引用,您将尝试再次处理它们!

您会注意到,在我的代码中,我小心地删除了对已释放对象的引用,所以我不会尝试对垃圾对象引用调用Dispose。但这并没有阻止一个微妙的bug潜入。

当用户调用Dispose()时:句柄CursorFileBitmapIconServiceHandle被销毁。稍后当垃圾收集器运行时,它将再次尝试销毁相同的句柄。

protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy 
   ...
}

解决这一问题的方法是告诉垃圾收集器,它不需要麻烦完成对象——它的资源已经被清理,不需要再做任何工作。您可以通过在Dispose()方法中调用GC.SuppressFinalize()来执行此操作:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
   GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}

现在用户调用了Dispose(),我们有:

释放的非托管资源释放的托管资源

GC运行终结器没有任何意义——所有事情都得到了处理。

我不能使用Finalize清理非托管资源吗?

Object.Finalize的文档说明:

Finalize方法用于在销毁当前对象之前对当前对象持有的非托管资源执行清理操作。

但MSDN文档还表示,对于IDisposable.Dispose:

执行与释放、释放或重置非托管资源相关联的应用程序定义任务。

那么是哪一个呢?哪一个是我清理非托管资源的地方?答案是:

这是你的选择!但选择Dispose。

您当然可以将非托管清理放置在终结器中:

~MyObject()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //A C# destructor automatically calls the destructor of its base class.
}

这样做的问题是,你不知道垃圾收集器什么时候可以完成你的对象。您的未管理、未需要、未使用的本机资源将一直存在,直到垃圾收集器最终运行。然后它将调用终结器方法;清理非托管资源。Object.Finalize的文档指出了这一点:

终结器执行的确切时间未定义。要确保类实例的资源的确定性释放,请实现Close方法或提供IDisposable.Dispose实现。

这是使用Dispose清理非托管资源的优点;您可以了解并控制何时清理非托管资源。它们的破坏是“确定性的”。


回答您最初的问题:为什么不现在释放内存,而不是在GC决定释放内存时释放内存?我有一个面部识别软件,现在需要去掉530 MB的内部图像,因为它们不再需要了。当我们不这样做时:机器会磨到交换停止。

奖金阅读

对于任何喜欢这个答案风格的人(解释原因,所以答案变得显而易见),我建议你阅读Don Box的基本COM的第一章:

直接链接:皮尔逊出版公司第1章样本磁铁:84bf0b960936d677190a2be355858e80ef7542c0

在35页中,他解释了使用二进制对象的问题,并在你眼前发明了COM。一旦你意识到COM的原因,剩下的300页就显而易见了,只需详细介绍一下微软的实现。

我认为每一个处理过对象或COM的程序员至少都应该阅读第一章。这是对任何事情最好的解释。

额外奖励阅读

当你所知道的一切都是错误的

因此,确实很难编写正确的终结器,我能给你的最好建议是不要尝试。