CSharp - 正确使用IDisposable接口

  显示原文与译文双语对照的内容

我知道从阅读msdn文档"主"使用idisposable接口清理非托管资源。

对我来说,"非托管"意味着像数据库连接,插座,窗口句柄,但是我看到了 等等,但是我看到了一些代码,这些代码是用来释放管理的资源的,因为垃圾收集器应该为你负责。

例如:


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;
 }

我的问题是,这是否使得垃圾收集器使用的内存比通常使用的内存快?

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

时间:

Dispose磅的磅为 。释放非托管资源。 它需要在某一点上完成,否则不会被清除。 垃圾收集器不知道如何调用 DeleteHandle()IntPtr 类型的一个变量,它不知道与否需要 DeleteHandle() 打电话。

注意:什么是非托管资源? 如果你在. NET 框架中找到它: 它被管理。如果你自己绕过 MSDN,它是不受管理的。 于清洗你up,任何你已经用p/相关调用以外的调用来获得所有商品的漂亮舒服的世界中为你提供的.NET 访问框架现在处于非托管状态–而你是 responsible.

你所创建的对象需要公开一些方法,这样外部世界才能调用,以便清理非托管资源。 这里方法甚至有一个标准化的名称:


public void Dispose()

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


public interface IDisposable
{
 void Dispose()
}

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


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

你已经完成了,除了你能做得更好。


如果你的对象已经分配了 250 MB的System.Drawing.Bitmap 。( 例如 ) 。 .NET 托管位图类) 作为某种帧缓冲区? 当然,这是一个托管的.NET 对象,垃圾收集器将释放它。 But do you really want to leave 250 MB of memory just sitting there, waiting for garbage collector to eventually last come along and free it? 如果有一个打开数据库连接? 当然我们不希望这个连接处于打开状态,等待GC最终完成对象。

如果用户已经调用了 Dispose() ( 意味着他们不再打算使用该对象),为什么不删除那些浪费的位图和数据库连接?

现在我们将:

  • 删除非托管资源( 因为我们必须),并
  • 摆脱托管资源( 因为我们希望有帮助)

因此,让我们更新 Dispose() 方法来摆脱那些被管理对象:


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

//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()? 然后他们会泄漏一些非托管资源!

注意:他们不会泄漏管理资源,因为最终垃圾收集器运行,后台线程,释放内存相关的任何未使用的对象。 这将包括你的对象以及你所使用的任何托管对象( 例如。 位图和 DbConnection ) 。

如果对方忘了打电话给 Dispose(),我们还可以 save ! 我们仍然有一种方法可以把它叫做 : 当垃圾收集器最终释放时( 例如 。 正在完成我们的对象。

注意:垃圾收集器将最终释放所有管理对象。 当它发生时,它调用对象上的英镑的Finalize 方法。 gc不知道,或者,关心你 处理方法。 这只是我们在想去掉非托管的东西时调用的一种方法。

破坏我们的对象被垃圾收集器完美时间释放那些讨厌的非托管资源。 我们通过重写 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!
}

但是这个代码中有一个 Bug 。 你看,垃圾收集器运行在后台线程;你不知道两个对象的顺序被摧毁。 完全有可能,在 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.gdiCursorBitmapStreamFileHandle);

//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
}

注意:如果你的对象是一个对象,实现了处理,然后不要忘记打电话给他们基地处理方法重写时处理:


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

和所有是好的,除了你能做得更好


如果用户在你的对象上调用 Dispose(),那么一切都被清除了。 稍后,当垃圾收集器出现并调用Finalize时,它会再次调用 Dispose

这不仅是浪费,但是如果你的对象有垃圾你已经处理的对象的引用从去年 Dispose(), 你将再次试图处理他们!

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

当用户调用 Dispose() 时: 处理的句柄为 gdiCursorBitmapStreamFileHandle 。 当垃圾收集器运行时,它将再次尝试销毁同一句柄。


protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
//Free unmanaged resources
 Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle); <--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中没有一点- - 所有的事情都是由。


要回答你最初的问题: 为什么现在不释放内存,而不是当GC决定执行它的时候? 我有一个面部识别软件,需要来摆脱 530 MB的内部图像现在 ,因为它们不再需要。 当我们不做时,机器将研磨到一个交换停止。

奖金读取

对于喜欢这里回答风格的任何人,我建议你阅读 box COM的第1 章:

在 35页中他解释了使用二进制对象的问题,并在你的眼睛之前发明了 com 。 一旦你意识到为什么com,其余 300页面是显而易见的,微软的详细实现。

我认为每个曾经处理过对象或者com的程序员至少应该阅读第一章。 这是有史以来最好的解释。

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相关的任何操作,因为清除一个 List 对正在处理的集合没有影响。 同样,将变量设置为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),,它实际上在两种不同情况下运行:

  • 处理 ==: 该方法被用户直接或者间接调用。 可以处理托管和非托管资源。
  • 处理==错误:该方法由运行时从终结器中调用,你不应该引用其他对象。 只能释放非托管资源。

仅仅让GC负责清理的问题是,你没有真正控制GC什么时候运行收集周期的真正控制,所以资源可能比需要的时间长。 记住,调用 Dispose() 实际上并不会导致收集循环,或者以任何方式导致GC收集/释放对象;它只是提供了更多deterministicly清理的方法,告诉GC这个清理已经完成。

IDisposable和dispose模式的作用不是立即释放内存。 调用Dispose的唯一时机实际上是在处理==错误场景并处理非托管资源时立即释放内存。 对于托管代码,在GC运行收集循环之前,内存实际上不会被回收,这实际上你无法控制( 除了调用我已经提到过的GC.Collect(), 之外,这不是一个好主意) 。

你的场景不是真正有效的,因为. NET 中的字符串不使用任何unamanged资源,并且没有实现 IDisposable,所以无法强制它们被"清除。"

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

关于管理/非托管的一般性问题以及其他问题的讨论,我认为对此问题的任何回答都必须从非托管资源的定义开始。

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

但这是关键- 它们可以是任何匹配的函数对。 一个建立一个状态,另一个会把它撕碎。 如果状态已经建立但还没有被破坏,那么资源的实例就会存在。 你必须安排卸载在正确的时间发生- 资源不是由CLR管理的。 唯一自动管理的资源类型是内存。 有两种类型:GC和堆栈。 值类型由堆栈( 或者通过在引用类型中搭便车) 管理,引用类型由GC管理。

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

查看有关公正的示例。 对文件的日志缩进的更改必须是完全嵌套的,否则会出错。 也不太可能是线程安全的。

可以与垃圾收集器一起挂起,使你的非托管资源得到清理。 但是只有当状态更改函数是线程安全的,并且两个状态可以有任何以任何方式重叠的生命周期时。 资源的正义示例不能有终结器 ! 这对谁都没有帮助。

对于那些资源,你可以只实现 IDisposable,而不需要终结器。 终结器绝对是可选的- 它必须是。 这在许多书中都被忽略甚至没有提到。

然后你必须使用 using 语句来确保 Dispose 被调用。 这基本上就像在堆栈( 因此,终结器是对GC的,using 是堆栈) 上搭便车一样。

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

还有一个选择,我更喜欢嵌套的状态,而不是线程安全的( 除了其他之外,避免将一个参数与一个不能在每个实现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");

λ是通过作为一个代码块,那么就像你自己的控制结构作为 using 服务于同样的目的,除了你不再有任何调用者滥用的危险。 他们没有办法清理资源。

这种技术是那么有用,如果资源是那种可能重叠的一生,因为你希望能够构建资源,资源b,然后杀死资源然后杀死资源b。 如果你强迫用户完全嵌套,你就不能这样做。 但是你需要使用 IDisposable ( 但仍然没有终结器,除非你已经实现了 threadsafety,它不是自由的) 。

是的,这个代码是完全多余的,而且没有必要,它不会让垃圾收集器做任何事情,除非是( 一旦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变得更加混乱,甚至可能使垃圾收集器已经执行的某些pre-calculated分析失效。

我使用 IDisposable 做一些事情,比如确保线程被正确处理,以及非托管资源。

对scott的评论进行了磅编辑

唯一一次gc性能指标影响的是当一个叫 [sic] GC.Collect() 了"

从概念上讲,GC维护对象引用图的视图,以及从线程的堆栈框架中对它的所有引用。 这个堆可以相当大,并且可以跨越很多页面。 作为优化,GC缓存它的对页面的分析,这些页面不太可能经常更改,以避免不必要地重新扫描页面。 当页面中的数据更改时,GC接收来自内核的通知,因此它知道页面是脏的,需要重新扫描。 如果集合在gen0那么很可能页面中其他的东西也在变化,但这是不太可能在gen1和代。 Anecdotally,这些钩子在 Mac OS X 中不可用,这个团队将GC移植到 Mac,以便让 Silverlight plug-in在这个平台上工作。

避免不必要的资源处置的另一点: 想象一个进程正在卸载的情况。 想象这个进程已经运行了一段时间。 很有可能进程页面的内存已经被交换到磁盘。 至少它们不再在L1或者L2缓存中。 在这种情况下,没有点卸载的应用程序交换这些数据和代码页回记忆'释放'资源将会发布的操作系统进程终止时无论如何。 这适用于托管的甚至某些非托管的资源。 只能释放保持non-background线程存活的资源,否则进程将保持活动状态。

现在,在正常执行期间,必须正确清理一些临时的资源,以避免非托管内存泄漏。 这些是必须处理的东西。 如果你创建了拥有线程( 我的意思是它创建了它,因此它负责确保它停止,至少通过我的编码风格)的类,那么该类很可能必须实现 IDisposable 并在 Dispose 期间删除线程。

.NET 框架使用 IDisposable 接口作为信号,甚至警告开发人员,这个类必须被处理。 我无法在框架中想到任何实现 IDisposable ( 排除显式接口实现)的类型,其中处理是可选的。

使用IDisposable的场景: 清理非托管资源,取消订阅事件,关闭连接

用于实现 IDisposable ( threadsafe )的习惯用法:


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
}

在你发布的示例中,它仍然不是"立即释放内存"。 所有内存都是垃圾收集,但它可能允许在早期的生成中收集内存。 你得做一些测试才能确定。


框架设计指南是指导方针,而不是规则。 他们告诉你界面是什么,什么时候使用,如何使用,什么时候使用。

我曾经读过代码,它是一个简单的RollBack(),在使用IDisposable时发生了故障。 下面的MiniTx类将检查 Dispose() 上的一个标志,如果 Commit 调用从未发生过,它就会调用 Rollback 本身。 它增加了一个间接层,使得调用代码更易于理解和维护。 结果类似于:


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来创建更干净的代码。

我不会重复使用或者释放un-managed资源的常用内容,这些都已经被覆盖。 但是,我想指出一个常见的误解。
给定以下代码

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

我意识到一次性实现并不遵循当前的原则,但是希望你们都能。
现在,当调用Dispose时,释放多少内存?

回答:无。
调用Dispose可以释放非托管资源,它不能回收托管内存,只有GC可以这样做。 这不是一个好主意,上面的模式仍然是一个好主意。 一旦运行,处理没有阻止gc被 _Large re-claiming内存,即使LargeStuff仍在范围的实例。 _Large中的字符串也可能在 0中,但LargeStuff的实例可能是 2,因此,内存会更快。
添加finaliser以调用上面显示的Dispose方法毫无意义。 这将只延迟内存的re-claiming以允许finaliser运行。

如果你想删除现在使用非托管内存。

请参见:

...