协慌网

登录 贡献 社区

正确使用 IDisposable 接口

我从阅读MSDN 文档中了解到, 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包含一百万个字符串,你想现在释放那个内存,而不是等待垃圾收集器。上面的代码会实现吗?

答案

Dispose 的目的释放非托管资源。它需要在某个时刻完成,否则它们将永远不会被清除。垃圾收集器不知道如何IntPtr类型的变量上调用DeleteHandle() ,它不知道它是否需要调用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()怎么办?然后他们会泄漏一些非托管资源!

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

如果这个人忘了叫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 。但这并没有阻止一个微妙的错误蔓延。

当用户调用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 的 Essential COM 第一章:

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

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

额外奖金阅读

当你知道的一切都被 Eric Lippert 弄错

因此,写一个正确的终结器是非常困难的, 我能给你的最好的建议是不要尝试

IDisposable通常用于利用using语句,并利用一种简单的方法对托管对象进行确定性清理。

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

Dispose 模式的目的是提供一种清理托管和非托管资源的机制,何时发生这种情况取决于如何调用 Dispose 方法。在您的示例中,Dispose 的使用实际上并没有执行与 dispose 相关的任何操作,因为清除列表对正在处理的集合没有影响。同样,将变量设置为 null 的调用也不会对 GC 产生影响。

您可以查看本文以获取有关如何实现 Dispose 模式的更多详细信息,但它基本上如下所示:

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

这里最重要的方法是 Dispose(bool),它实际上在两种不同的情况下运行:

  • disposing == true:该方法由用户代码直接或间接调用。可以处理托管和非托管资源。
  • disposing == false:运行时从终结器内部调用该方法,不应引用其他对象。只能处理非托管资源。

简单地让 GC 负责清理的问题在于你无法真正控制 GC 何时运行一个收集周期(你可以调用 GC.Collect(),但你真的不应该这样做)所以资源可能会停留比需要的时间更长。请记住,调用 Dispose()实际上不会导致收集周期或以任何方式导致 GC 收集 / 释放对象; 它只是提供了更加确定性地清理所用资源的方法,并告诉 GC 已经执行了这次清理。

IDisposable 和处理模式的重点不在于立即释放内存。调用 Dispose 实际上甚至有可能立即释放内存的唯一一次是它处理 disposing == false 场景并操纵非托管资源。对于托管代码,内存实际上不会被回收,直到 GC 运行一个收集周期,你实际上无法控制(除了调用 GC.Collect(),我已经提到过这不是一个好主意)。

您的方案并不真正有效,因为. NET 中的字符串不使用任何未管理的资源而且没有实现 IDisposable,因此无法强制它们被 “清理”。