正确使用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使用的垃圾回收器可用内存比MyCollection更快?

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


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

注意 :什么是非托管资源? 如果您在Microsoft .NET Framework中找到它:它是托管的。 如果你自己去了解MSDN,那么它是非托管的。 任何你使用过P / Invoke调用的东西都不在.NET框架中可用的所有可用的舒适的世界之外,而且你现在负责清理它。

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

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 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的基本COM的第一章:

  • 直接链接:Pearson Publishing的第1章样本
  • 磁铁:84bf0b960936d677190a2be355858e80ef7542c0
  • 在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的使用实际上并未执行与处置有关的任何操作,因为清除列表对处理该集合没有影响。 同样,将变量设置为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),它实际上在两种不同的情况下运行:

  • 处置== true:该方法直接或间接由用户代码调用。 可以处置托管和非托管资源。
  • 处置== false:该方法已由运行时从终结器中调用,并且不应引用其他对象。 只能处理非托管资源。
  • 简单地让GC负责清理的问题是,您无法真正控制GC何时运行一个收集周期(您可以调用GC.Collect(),但实际上不应该这样做),因此资源可能会停留比需要的时间长。 请记住,调用Dispose()实际上并不会导致收集周期或以任何方式导致GC收集/释放对象; 它只是提供了更确定性地清理所使用的资源的方法,并告诉GC该清理已经执行。

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

    你的场景并不真正有效,因为.NET中的字符串没有使用任何未经处理的资源,也没有实现IDisposable,所以没有办法强制它们被“清理”。

    链接地址: http://www.djcxy.com/p/1593.html

    上一篇: Proper use of the IDisposable interface

    下一篇: What does the [Flags] Enum Attribute mean in C#?