正确使用 IDisposable 接口

Proper use of the IDisposable interface

提问人:cwick 提问时间:2/12/2009 最后编辑:spaleetcwick 更新时间:4/9/2023 访问量:419479

问:

我从阅读 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

C# .NET 垃圾回收 IDISPOSABLE

评论

43赞 Punit Vora 10/2/2010
我喜欢公认的答案,因为它告诉你使用 IDisposable 的正确“模式”,但就像 OP 在他的编辑中所说的那样,它没有回答他想要的问题。IDisposable 不会“调用”GC,它只是将对象“标记”为可销毁。但是,“立即”释放内存而不是等待 GC 启动的真正方法是什么?我认为这个问题值得更多讨论。
53赞 John Saunders 10/2/2010
IDisposable不标记任何东西。该方法执行它必须执行的操作来清理实例使用的资源。这与GC无关。Dispose
5赞 Punit Vora 10/7/2010
@John。我确实明白.这就是为什么我说接受的答案没有回答 OP 的预期问题(和后续编辑),即 IDisposable 是否有助于 <i>释放内存</i>。由于与释放内存无关,只与资源有关,因此就像您所说,根本不需要将托管引用设置为 null,这就是 OP 在他的示例中所做的。因此,他的问题的正确答案是“不,它无助于更快地释放内存。事实上,它根本无助于释放内存,只会释放资源”。但无论如何,感谢您的输入。IDisposableIDisposable
13赞 John Saunders 10/7/2010
@desigeek:如果是这种情况,那么你不应该说“IDisposable 不会'调用'GC,它只是'标记'一个对象是可销毁的”
7赞 Concrete Gannet 8/14/2015
@desigeek:无法保证确定性地释放内存。你可以打电话给GC。Collect(),但这是一个礼貌的请求,而不是一个要求。必须暂停所有正在运行的线程才能进行垃圾回收 - 如果您想了解更多信息,请阅读 .NET 安全点的概念,例如 msdn.microsoft.com/en-us/library/678ysw69(v=vs.110).aspx 。如果线程无法挂起,例如,因为调用了非托管代码,则 GC。Collect() 可能什么都不做。

答:

13赞 mqp 2/12/2009 #1

是的,该代码是完全冗余和不必要的,它不会使垃圾回收器执行任何它不会执行的操作(一旦 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()

评论

3赞 Jesse Chisholm 8/31/2012
回复:“内存将在需要时释放。不如说,“当 GC 决定需要它时”。在 GC 决定真正需要内存之前,您可能会看到系统性能问题。现在释放它可能不是必需的,但可能有用。
1赞 supercat 4/26/2013
在某些极端情况下,取消集合中的引用可能会加快由此引用的项的垃圾回收。例如,如果创建了一个大型数组并填充了对新创建的较小项的引用,但之后很长一段时间都不需要它,则放弃该数组可能会导致这些项保留到下一个 2 级 GC,而首先将其清零可能会使这些项符合下一个 0 级或 1 级 GC 的条件。可以肯定的是,无论如何,在大型对象堆上拥有大型短寿命对象是令人讨厌的(我不喜欢这种设计),但是......
1赞 supercat 4/26/2013
...在放弃这些数组之前将其清零有时会减少对 GC 的影响。
0赞 AyCe 9/13/2021
在大多数情况下,不需要清空内容,但某些对象实际上也可能使一堆其他对象保持活动状态,即使它们不再需要。将对 Thread 的引用设置为 null 可能是有益的,但现在可能不是。通常,如果大对象仍然可以在某种检查它是否已经为空的方法中调用,那么更复杂的代码不值得提高性能。更喜欢干净而不是“我认为这稍微快一点”。
81赞 yfeldblum 2/12/2009 #2

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.");
        }
    }
}
12赞 Drew Noakes 2/12/2009 #3

如果无论如何都要进行垃圾回收,那么您就不需要处理它。这样做只会使 CPU 的搅动超过必要的程度,甚至可能使垃圾回收器已经执行的某些预先计算的分析无效。MyCollection

我过去常常做一些事情,比如确保线程以及非托管资源被正确处置。IDisposable

编辑针对斯科特的评论:

GC 性能指标唯一受到影响的情况是调用 [sic] GC.Collect() 已生成”

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

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

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

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

评论

0赞 Scott Dorman 2/12/2009
调用 Dispose 是完全有效、合法和鼓励的。实现 IDisposable 的对象通常这样做是有原因的。GC 性能指标唯一受到影响的情况是调用 GC.Collect() 被制作。
0赞 supercat 8/2/2011
对于许多 .net 类来说,处置是“某种程度上”可选的,这意味着只要人们不疯狂地创建新实例并放弃它们,放弃实例“通常”不会造成任何麻烦。例如,编译器生成的控件代码似乎在实例化控件时创建字体,并在释放窗体时放弃它们;如果创建和释放数千个控件,这可能会占用数千个 GDI 句柄,但在大多数情况下,控件不会创建和销毁那么多。尽管如此,人们仍然应该努力避免这种放弃。
1赞 supercat 8/2/2011
就字体而言,我怀疑问题在于 Microsoft 从未真正定义过哪个实体负责处置分配给控件的“字体”对象;在某些情况下,控件可能与生存期较长的对象共享字体,因此让控件释放字体会很糟糕。在其他情况下,字体将分配给控件,而不是其他位置,因此,如果控件不释放它,则没有人会释放它。顺便说一句,如果有一个单独的非一次性 FontTemplate 类,字体的这种困难是可以避免的,因为控件似乎不使用其字体的 GDI 句柄。
51赞 Scott Dorman 2/12/2009 #4

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),它实际上在两种不同的环境中运行:

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

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

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

你的方案实际上并不有效,因为 .NET 中的字符串不使用任何未识别的资源,也不实现 IDisposable,因此无法强制“清理”它们。

评论

0赞 toha 2/22/2023
如果我更改 if (handle != null) { handle.Dispose();} to if (handle != null) { handle = null;这有什么区别吗?
2赞 Michael Burr 2/12/2009 #5

该操作在示例代码中执行的某些操作可能会产生由于对象的正常 GC 而不会发生的效果。Dispose()MyCollection

如果对象被其他对象引用或被其他对象引用,则该对象将不受收集的约束,但会突然没有内容。如果没有示例中的 Dispose() 操作,这些集合仍将包含其内容。_theList_theDictList<>Dictionary<>

当然,如果是这种情况,我会称其为破碎的设计 - 我只是指出(我想是迂腐的)该操作可能不是完全多余的,具体取决于是否有其他用途 or 未在片段中显示。Dispose()List<>Dictionary<>

评论

0赞 mqp 2/12/2009
它们是私有字段,所以我认为假设 OP 没有给出对它们的引用是公平的。
0赞 Michael Burr 2/12/2009
1)代码片段只是示例代码,所以我只是指出可能存在容易被忽视的副作用;2) 私有字段通常是 getter 属性/方法的目标 - 可能太多了(getter/setter 被一些人认为有点反模式)。
2934赞 43 revs, 23 users 80%Ian Boyd #6

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

注意:什么是非托管资源?如果您在 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# 中,不会显式重写该方法。 您编写了一个看起来像 C++ 析构函数的方法,并且 编译器将其视为该方法的实现:Finalize()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()

执行此操作的标准模式是 have 并且 both 调用 third(!) 方法;如果你从(而不是 )调用布尔值,则传递布尔值,这意味着释放托管资源是安全的。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();
    }
}

一切都很好,除了你可以做得更好


如果用户调用您的对象,则所有内容都已清理。稍后,当垃圾回收器出现并调用 Finalize 时,它将再次调用。Dispose()Dispose

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

您会注意到,在我的代码中,我小心翼翼地删除了对我已处置的对象的引用,因此我不会尝试调用垃圾对象引用。但这并没有阻止一个微妙的错误悄悄出现。Dispose

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

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

奖励阅读

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

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

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

额外奖励阅读

当你所知道的一切都是错误的档案埃里克·利珀特

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

评论

45赞 integra753 2/9/2012
这是一个很好的答案,但我认为它会受益于标准案例的最终代码列表,以及该类派生自已经实现 Dispose 的基类的情况。例如,在这里阅读了(msdn.microsoft.com/en-us/library/aa720161%28v=vs.71%29.aspx),我对从已经实现 Dispose 的类派生时应该做什么感到困惑(嘿,我是新手)。
3赞 Ian Boyd 9/13/2021
@Ayce “如果你写了正确的代码,你就永远不需要终结器/Dispose(bool) 的东西。”我不是在保护我;我正在保护数十、数百、数千或数百万其他开发人员,他们可能不会每次都做对。有时开发人员忘记调用 .有时您不能使用 .我们遵循“成功之坑”的 .NET/WinRT 方法。我们向开发人员缴纳税款,并编写更好的防御性代码,使其能够适应这些问题。.Disposeusing
3赞 Ian Boyd 9/14/2021
“但你并不总是需要为'公众'编写代码。但是,当尝试为 2k+ 赞成的答案提出最佳实践时,最好尽可能提供最好的代码示例。我们不想把一切都排除在外——让人们以艰难的方式绊倒这一切。因为这就是现实 - 每年有成千上万的开发人员学习有关处置的这种细微差别。没有必要让他们不必要地更难。
2赞 Ian Boyd 2/22/2023
@toha 这是两种方法。实际上,您可以忽略其中之一,只剩下 - 您只清理非托管资源。您不必重写终结器并尝试在终结期间调用 - 使用您的类的开发人员需要(强制,没有 ifs、and 或 buts)来调用自己。如果他们没有打电话:那么他们不遵守基本规则。剩下的答案是针对那些想要尝试使他们的类对糟糕的程序员更具弹性的开发人员。(即“你可以做得更好”)DisposeDispose.Dispose.Dispose
2赞 Keyrad 3/20/2023
这是我读过的最好的答案。@IanBoyd 感谢您抽出宝贵的时间给出如此有见地和足智多谋的答案,您值得拥有这些赞票!
18赞 olli-MSFT 2/12/2009 #7

我使用 IDisposable 的方案:清理非托管资源、取消订阅事件、关闭连接

我用于实现 IDisposable(非线程安全)的惯用语:

class MyClass : IDisposable {
    // ...

    #region IDisposable Members and Helpers
    private bool disposed = false;

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

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                // cleanup code goes here
            }
            disposed = true;
        }
    }

    ~MyClass() {
        Dispose(false);
    }
    #endregion
}

评论

1赞 MikeJ 1/26/2021
这几乎是 Microsoft Dispose 模式的实现,只是您忘记了将 DIspose(bool) 虚拟化。该模式本身不是一个非常好的模式,应该避免使用,除非您绝对必须将 dispose 作为继承层次结构的一部分。
24赞 Daniel Earwicker 2/12/2009 #8

在调用 Dispose 之后,不应再调用对象的方法(尽管对象应允许对 Dispose 的进一步调用)。因此,问题中的例子是愚蠢的。如果调用 Dispose,则可以丢弃对象本身。因此,用户应该放弃对整个对象的所有引用(将它们设置为 null),并且它内部的所有相关对象都将自动清理。

至于关于托管/非托管的一般问题以及其他答案中的讨论,我认为这个问题的任何答案都必须从非托管资源的定义开始。

归根结底,你可以调用一个函数来使系统进入状态,你可以调用另一个函数来使它脱离该状态。现在,在典型示例中,第一个可能是返回文件句柄的函数,第二个可能是对 的调用。CloseHandle

但是 - 这是关键 - 它们可以是任何匹配的函数对。一个建立国家,另一个摧毁它。如果状态已生成但尚未拆除,则存在资源的实例。您必须安排在正确的时间进行拆解 - 资源不受 CLR 管理。唯一自动管理的资源类型是内存。有两种类型:GC 和堆栈。值类型由堆栈管理(或通过在引用类型中搭便车),引用类型由 GC 管理。

这些函数可能会导致状态更改,这些更改可以自由交错,或者可能需要完全嵌套。状态更改可能是线程安全的,也可能不是。

看看贾斯蒂斯问题中的例子。对日志文件缩进的更改必须完全嵌套,否则一切都会出错。此外,它们不太可能是线程安全的。

可以搭上垃圾回收器的顺风车,清理非托管资源。但前提是状态更改函数是线程安全的,并且两个状态的生存期可以以任何方式重叠。因此,正义的资源示例一定不能有终结者!它只是对任何人都无济于事。

对于这些类型的资源,您可以只实现 ,而无需终结器。终结器是绝对可选的 - 它必须是。这在许多书中都被掩盖了,甚至没有提到。IDisposable

然后,您必须使用该语句来确保它被调用。这基本上就像搭上堆栈的顺风车(因此,终结器之于 GC,即堆栈)。usingDisposeusing

缺少的部分是您必须手动编写 Dispose 并使其调用您的字段和基类。 C++/CLI 程序员不必这样做。在大多数情况下,编译器会为他们编写它。

还有一种替代方法,我更喜欢完美嵌套且不是线程安全的状态(除此之外,避免 IDisposable 可以避免与无法抗拒向实现 IDisposable 的每个类添加终结器的人发生争执的问题)。

您不是编写类,而是编写函数。该函数接受要回调的委托:

public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

然后一个简单的例子是:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

传入的 lambda 用作代码块,因此就像您创建自己的控制结构以服务于 相同的目的,只是您不再有任何调用者滥用它的危险。他们不可能不清理资源。using

如果资源是可能具有重叠生存期的资源类型,则此方法不太有用,因为这样您希望能够先生成资源 A,然后生成资源 B,然后终止资源 A,然后再终止资源 B。如果你强迫用户像这样完美地嵌套,你就不能这样做。但是你需要使用(但仍然没有终结器,除非你已经实现了线程安全,这不是免费的)。IDisposable

5赞 Arjan Einbu 2/12/2009 #9

如果有的话,我希望代码的效率低于将其排除在外时。

调用 Clear() 方法是不必要的,如果 Dispose 不这样做,GC 可能不会这样做......

7赞 Robert Paulson 2/12/2009 #10

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


框架设计指南是指南,而不是规则。它们告诉您界面的主要用途、何时使用它、如何使用它以及何时不使用它。

我曾经读过一个简单的代码 RollBack() 使用 IDisposable 失败。下面的 MiniTx 类将检查 Dispose() 上的标志,如果调用从未发生,它将调用自身。它增加了一层间接性,使调用代码更易于理解和维护。结果如下所示:CommitRollback

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 创建更简洁的代码。

评论

0赞 Aluan Haddad 9/22/2016
看看 @Daniel Earwicker 使用高阶函数的示例。用于基准测试、计时、日志记录等。这似乎更简单。
8赞 pipTheGeek 2/12/2009 #11

我不会重复关于使用或释放非托管资源的通常内容,这些内容都已涵盖。但我想指出一个似乎很普遍的误解。
给定以下代码

Public Class LargeStuff
  Implements IDisposable
  Private _Large as string()

  'Some strange code that means _Large now contains several million long strings.

  Public Sub Dispose() Implements IDisposable.Dispose
    _Large=Nothing
  End Sub

我意识到 Disposable 实现没有遵循当前的准则,但希望你们都能理解这个想法。
现在,当调用 Dispose 时,会释放多少内存?

答:没有。
调用 Dispose 可以释放非托管资源,它不能回收托管内存,只有 GC 可以这样做。这并不是说上述不是一个好主意,实际上遵循上述模式仍然是一个好主意。运行 Dispose 后,即使 LargeStuff 实例可能仍在范围内,也没有什么可以阻止 GC 重新回收_Large正在使用的内存。_Large 中的字符串也可能在第 0 代中,但 LargeStuff 的实例可能是第 2 代,因此,内存将很快被回收。
但是,添加终结器来调用上面所示的 Dispose 方法是没有意义的。这只会延迟重新回收内存以允许终结器运行。

评论

1赞 supercat 5/10/2013
如果一个实例已经存在了足够长的时间,可以进入第 2 代,并且如果保留了对第 0 代中新创建的字符串的引用,那么如果 的实例被放弃而没有清空,那么引用的字符串将被保留到下一个 Gen2 集合。清零可能会让字符串在下一个 Gen0 集合中被消除。在大多数情况下,取消引用是没有帮助的,但在某些情况下,它可以提供一些好处。LargeStuff_LargeLargeStuff_Large_Large_Large
3赞 Adam Speight 2/21/2012 #12

IDisposable适用于取消订阅事件。

2赞 supercat 2/22/2012 #13

大多数关于“非托管资源”的讨论的一个问题是,它们并没有真正定义这个术语,但似乎暗示它与非托管代码有关。虽然许多类型的非托管资源确实与非托管代码交互,但从这些术语中考虑非托管资源是没有帮助的。

相反,人们应该认识到所有托管资源的共同点:它们都要求某个外部“事物”代表它做某事,从而损害其他一些“事物”,而另一个实体同意这样做,直到另行通知。如果这个物体被遗弃并消失得无影无踪,那么在“事物”之外,没有什么东西会告诉它,它不再需要代表不再存在的物体改变它的行为;因此,“东西”的有用性将被永久削弱。

因此,非托管资源代表了某个外部“事物”代表对象改变其行为的协议,如果该对象被放弃并不再存在,这将无用地损害该外部“事物”的有用性。托管资源是此类协议的受益人,但已签署在被遗弃时接收通知的对象,并将使用此类通知在销毁之前整理其事务。

评论

0赞 eonil 5/27/2014
好吧,IMO,非托管对象的定义很清楚;任何非 GC 对象
1赞 supercat 5/27/2014
@Eonil:非托管对象 != 非托管资源。像事件这样的事情可以完全使用托管对象来实现,但仍然构成非托管资源,因为至少在短期对象订阅长期对象事件的情况下,GC 对如何清理它们一无所知。
7赞 franckspike 6/4/2013 #14

如果要立即删除,请使用非托管内存

看:

5赞 Pragmateek 6/15/2013 #15

除了主要用作控制系统资源生存期的一种方式(完全包含在 Ian 的精彩回答中,荣誉!)之外,IDisposable/using 组合还可用于确定(关键)全局资源的状态更改范围控制台线程进程、任何全局对象(如应用程序实例)。

我写了一篇关于这种模式的文章:http://pragmateek.com/c-scope-your-global-state-changes-with-idisposable-and-the-using-statement/

它说明了如何以可重用可读的方式保护一些常用的全局状态:控制台颜色、当前线程区域性Excel 应用程序对象属性......

2赞 Yuriy Zaletskyy 8/31/2015 #16

首先是定义。对我来说,非托管资源意味着某个类,它实现了 IDisposable 接口或使用对 dll 的调用创建的东西。GC 不知道如何处理此类对象。例如,如果类只有值类型,那么我不会将此类视为具有非托管资源的类。 对于我的代码,我遵循以下做法:

  1. 如果由我创建的类使用一些非托管资源,那么这意味着我还应该实现 IDisposable 接口以清理内存。
  2. 使用完后立即清洁对象。
  3. 在我的 dispose 方法中,我遍历类的所有 IDisposable 成员并调用 Dispose。
  4. 在我的 Dispose 方法中调用 GC。SuppressFinalize(this) 以通知垃圾回收器我的对象已被清理。我这样做是因为调用 GC 是昂贵的操作。
  5. 作为额外的预防措施,我尝试多次调用 Dispose()。
  6. 有时我添加了私有成员_disposed并签入方法调用是否清理了对象。如果它被清理了,则生成 ObjectDisposedException
    以下模板演示了我用文字描述的代码示例:

public class SomeClass : IDisposable
    {
        /// <summary>
        /// As usually I don't care was object disposed or not
        /// </summary>
        public void SomeMethod()
        {
            if (_disposed)
                throw new ObjectDisposedException("SomeClass instance been disposed");
        }

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

        private bool _disposed;

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;
            if (disposing)//we are in the first call
            {
            }
            _disposed = true;
        }
    }

评论

1赞 Aluan Haddad 9/22/2016
“对我来说,非托管资源意味着某个类,它实现了 IDisposable 接口或使用对 dll 的调用创建的东西。”所以你是说任何类型本身都应该被视为非托管资源?这似乎不正确。此外,如果实现类型是纯值类型,您似乎建议不需要释放它。这似乎也是错误的。is IDisposable
0赞 Yuriy Zaletskyy 4/21/2018
每个人都自己判断。我不喜欢仅仅为了添加而将一些东西添加到我的代码中。这意味着如果我添加 IDisposable,则意味着我创建了某种 GC 无法管理的功能,或者我认为它将无法正确管理其生存期。
1赞 controlbox 9/14/2016 #17

处置托管资源的最合理用例是准备 GC 回收否则永远不会收集的资源。

一个典型的例子是循环引用。

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

解决此问题的唯一方法是通过将子项上的父项引用设置为 null 来手动中断循环引用。

在父级和子级上实现 IDisposable 是执行此操作的最佳方法。当对 Parent 调用 Dispose 时,对所有 Children 调用 Dispose,并在子 Dispose 方法中,将 Parent 引用设置为 null。

评论

4赞 supercat 9/15/2016
在大多数情况下,GC 不是通过识别死物来工作的,而是通过识别活物来工作的。在每个 gc 周期之后,对于已注册完成、存储在大型对象堆上或作为实时目标的每个对象,系统将检查一个标志,该标志指示在上一个 GC 周期中找到了实时根引用,并将该对象添加到需要立即完成的对象队列中, 从大型对象堆中释放对象,或使弱引用失效。如果不存在其他引用,则循环引用不会使对象保持活动状态。WeakReference
3赞 CharithJ 5/26/2017 #18

给定的代码示例不是一个很好的使用示例。字典清除通常不应转到该方法。当字典项目超出范围时,将清除并释放字典项目。 需要实现来释放一些内存/处理程序,这些内存/处理程序即使在超出范围后也不会释放/释放。IDisposableDisposeIDisposable

下面的示例演示了包含一些代码和注释的 IDisposable 模式的一个很好的示例。

public class DisposeExample
{
    // A base class that implements IDisposable. 
    // By implementing IDisposable, you are announcing that 
    // instances of this type allocate scarce resources. 
    public class MyResource: IDisposable
    {
        // Pointer to an external unmanaged resource. 
        private IntPtr handle;
        // Other managed resource this class uses. 
        private Component component = new Component();
        // Track whether Dispose has been called. 
        private bool disposed = false;

        // The class constructor. 
        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // Implement IDisposable. 
        // Do not make this method virtual. 
        // A derived class should not be able to override this method. 
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method. 
            // Therefore, you should call GC.SupressFinalize to 
            // take this object off the finalization queue 
            // and prevent finalization code for this object 
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios. 
        // If disposing equals true, the method has been called directly 
        // or indirectly by a user's code. Managed and unmanaged resources 
        // can be disposed. 
        // If disposing equals false, the method has been called by the 
        // runtime from inside the finalizer and you should not reference 
        // other objects. Only unmanaged resources can be disposed. 
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called. 
            if(!this.disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources. 
                if(disposing)
                {
                    // Dispose managed resources.
                    component.Dispose();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here. 
                // If disposing is false, 
                // only the following code is executed.
                CloseHandle(handle);
                handle = IntPtr.Zero;

                // Note disposing has been done.
                disposed = true;

            }
        }

        // Use interop to call the method necessary 
        // to clean up the unmanaged resource.
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // Use C# destructor syntax for finalization code. 
        // This destructor will run only if the Dispose method 
        // does not get called. 
        // It gives your base class the opportunity to finalize. 
        // Do not provide destructors in types derived from this class.
        ~MyResource()
        {
            // Do not re-create Dispose clean-up code here. 
            // Calling Dispose(false) is optimal in terms of 
            // readability and maintainability.
            Dispose(false);
        }
    }
    public static void Main()
    {
        // Insert code here to create 
        // and use the MyResource object.
    }
}
4赞 MikeJ 10/4/2018 #19

我看到很多答案已经转向谈论将 IDisposable 用于托管和非托管资源。我建议将这篇文章作为我发现的关于如何实际使用 IDisposable 的最佳解释之一。

https://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

对于实际问题;如果使用 IDisposable 清理占用大量内存的托管对象,则简短的回答是否的。原因是,一旦保存内存的对象超出范围,它就可以进行收集了。此时,任何引用的子对象也超出了范围,将被收集。

唯一真正的例外是,如果托管对象中占用了大量内存,并且阻塞了该线程等待某些操作完成。如果在调用完成后不需要这些对象,则将这些引用设置为 null 可能会允许垃圾回收器更快地收集它们。但这种情况将代表需要重构的错误代码 - 而不是 IDisposable 的用例。

评论

1赞 Sebastian Oscar Lopez 8/23/2019
我不明白为什么有人在你的答案中加上 -1
0赞 Lawrence Thurman 7/2/2021
我看到的一个问题是人们一直认为使用 using 语句打开文件会使用 Idisposable。当 using 语句完成时,它们不会关闭,因为 GC 将垃圾回收调用处置、yada yada 和文件将被关闭。相信我,确实如此,但速度还不够快。有时,需要立即重新打开同一文件。这就是 VS 2019 .Net Core 5.0 中目前发生的事情
0赞 MikeJ 7/2/2021
@LawrenceThurman,您似乎在描述人们使用一次性设备,而没有 using 语句,而是在具有终结器的类上。GC 不调用 dispose,而是调用终结器。例如,如果 FIleStream 包装在 using 语句中,则在释放时将关闭文件。
0赞 Lawrence Thurman 7/8/2021
@MikeJ试试看 - 我向你保证我知道我在说什么。使用 using 语句打开一个文件,将其修改为 close,然后立即尝试重新打开同一文件并再次修改它。现在连续这样做 30 次。我曾经每小时处理 750,000 个 jpg 来构建构建 pdf,并将原始彩色 jpg 转换为黑白。jpgs。这些 Jpg 是从账单上扫描的页面,有些有 10 页。GC 速度很慢,尤其是当您拥有一台具有 256 GB 内存的机器时。当机器需要更多内存时,它会收集,
0赞 Lawrence Thurman 7/8/2021
它只查找未使用的对象,当它看起来时。您需要调用 file。Close() 在 using 语句末尾之前。哦,是的,也可以尝试使用数据库连接,使用实数,800,000 个连接,你知道就像大型银行可能会使用的那样,这就是人们使用连接池的原因。
0赞 T Brown 2/18/2023 #20

我认为人们将 IDisposable 的 PATTERN 与 IDisposable 的主要目的混为一谈,后者旨在帮助清理非托管资源。我们都知道这一点。有些人认为这种模式具有某种神奇的力量,可以清除记忆并释放资源。PATTERN 不会这样做。但是,将该模式与实现的方法一起使用确实会清除内存并释放资源。

该模式只是一个内置的 try{} finally{} 块。而已。仅此而已。那么这意味着什么呢?您可以创建一个代码块,让您在最后执行某些操作,而无需为其执行额外的代码。它提供了一个 CUSTOM 块,可用于对代码和范围进行分段。

我的例子:

//My way
using (var _ = new Metric("My Test"))
{
    DoSomething();  //You now know all work in your block is being timed.
}

//MS mockup from memory
var sw = new Stopwatch();
sw.Start();
DoSomething();  //something fails? I never get the elapsed time this way
sw.Stop();

Metric 类

    public class Metric : IDisposable
    {
        private string _identifier;
        private DateTime _start;

        public Metric(string identifier)
        {
            _identifier = identifier;
            _start = DateTime.Now;
        }

        public void Dispose()
        {
            Console.WriteLine(_identifier + " - " + (DateTime.Now - _start).TotalMilliseconds)
        }
    }