一尘不染

正确使用 IDisposable 接口

javascript

我通过阅读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想释放该内存,而不是等待垃圾收集器。上面的代码能做到这一点吗?


阅读 128

收藏
2022-02-16

共1个答案

一尘不染

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

注意:什么是非托管资源?如果您在 Microsoft .NET Framework 中找到它:它是托管的。如果您自己浏览 MSDN,它是不受管理的。您使用 P/Invoke 调用来摆脱 .NET Framework 中可用的一切的美好舒适世界的任何东西都是非托管的——您现在负责清理它。

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

public void Cleanup()

或者

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()你的对象怎么办?然后他们会泄漏一些非托管资源!

注意:它们不会泄漏托管资源,因为最终垃圾收集器将在后台线程上运行,并释放与任何未使用对象关联的内存。这将包括您的对象以及您使用的任何托管对象(例如 theBitmap和 the 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垃圾对象引用。但这并没有阻止一个微妙的错误潜入。

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

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

你解决这个问题的方法是告诉垃圾收集器它不需要费心完成对象——它的资源已经被清理了,不需要更多的工作。您可以通过调用GC.SuppressFinalize()以下Dispose()方法来做到这一点:

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 的内部图像,因为它们不再需要。当我们不这样做时:机器会停止交换。

2022-02-16