我从阅读MSDN文档中了解到,IDisposable
接口的"主要"用途是清理非托管资源.
对我来说,"非托管"意味着数据库连接,套接字,窗口句柄等等.但是,我已经看到了Dispose()
实现该方法以释放托管资源的代码,这对我来说似乎是多余的,因为垃圾收集器应该照顾那对你而言.
例如:
public class MyCollection : IDisposable { private List_theList = new List (); private Dictionary _theDict = new Dictionary (); // Die, clear it up! (free unmanaged resources) public void Dispose() { _theList.clear(); _theDict.clear(); _theList = null; _theDict = null; }
我的问题是,这是否使得垃圾收集器可以使用的内存MyCollection
比通常更快?
编辑:到目前为止,人们已经发布了一些使用IDisposable清理非托管资源(例如数据库连接和位图)的好例子.但是假设_theList
在上面的代码中包含了一百万个字符串,并且你想现在释放那个内存,而不是等待垃圾收集器.上面的代码会实现吗?
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()
给你的对象怎么办?然后他们会泄漏一些非托管资源!
注意:它们不会泄漏托管资源,因为最终垃圾收集器将在后台线程上运行,并释放与任何未使用对象关联的内存.这将包括您的对象以及您使用的任何托管对象(例如,
Bitmap
和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运行终结器没有意义 - 一切都在处理.
文档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第一章:
直接链接: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的使用实际上并没有执行与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,因此无法强制它们被"清理".
在调用Dispose之后,不应再调用对象的方法(尽管对象应该容忍对Dispose的进一步调用).因此问题中的例子很愚蠢.如果调用Dispose,则可以丢弃对象本身.因此,用户应该放弃对该整个对象的所有引用(将它们设置为null),并且内部的所有相关对象将自动清除.
至于关于托管/非托管的一般问题以及其他答案中的讨论,我认为对这个问题的任何答案都必须从非托管资源的定义开始.
它归结为有一个函数你可以调用将系统置于一个状态,还有一个函数你可以调用它将它带回该状态.现在,在典型示例中,第一个可能是返回文件句柄的函数,第二个可能是调用CloseHandle
.
但是 - 这是关键 - 它们可以是任何匹配的功能对.一个建立一个国家,另一个撕毁它.如果状态已经构建但尚未拆除,则存在资源的实例.您必须安排在正确的时间进行拆解 - 资源不由CLR管理.唯一自动管理的资源类型是内存.有两种:GC和堆栈.值类型由堆栈管理(或通过在引用类型内搭接),引用类型由GC管理.
这些函数可能导致状态更改,可以自由交错,或者可能需要完美嵌套.状态更改可能是线程安全的,也可能不是.
看看Justice的问题中的例子.对日志文件缩进的更改必须完全嵌套,否则一切都会出错.他们也不太可能是线程安全的.
可以与垃圾收集器搭便车,以清理您的非托管资源.但是,只有状态更改函数是线程安全的,并且两个状态可以具有以任何方式重叠的生命周期.因此,正义的资源示例必须没有终结者!这对任何人都没有帮助.
对于这些类型的资源,您可以在IDisposable
没有终结器的情况下实现.终结者绝对是可选的 - 必须是.这在许多书中被掩盖或甚至没有提及.
然后,您必须使用该using
语句才有可能确保Dispose
调用该语句.这基本上就像搭便车(因为终结器是GC,using
是堆栈).
缺少的部分是你必须手动编写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
(但仍然没有终结器,除非你已经实现了线程安全,这不是免费的).
方案我使用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 }
是的,该代码是完全冗余和不必要的,并且它不会使垃圾收集器做任何其他事情都不会做的事情(一旦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超过必要的流失,甚至可能使垃圾收集器已经执行的一些预先计算的分析无效.
我IDisposable
用来做一些事情,比如确保正确处理线程,以及非托管资源.
编辑回应斯科特的评论:
GC性能指标受影响的唯一时间是调用[sic] GC.Collect()的时间"
从概念上讲,GC维护对象引用图的视图,以及从线程的堆栈帧对它的所有引用.这个堆可能非常大并且跨越许多页面的内存.作为优化,GC会缓存对不太可能经常更改的页面的分析,以避免不必要地重新扫描页面.当页面中的数据发生更改时,GC会从内核接收通知,因此它知道页面很脏并需要重新扫描.如果集合在Gen0中,那么页面中的其他内容可能也会发生变化,但这在Gen1和Gen2中的可能性较小.有趣的是,这些挂钩在Mac OS X中不适用于将GC移植到Mac以便在该平台上运行Silverlight插件的团队.
反对不必要的资源处置的另一点:想象一个过程正在卸载的情况.想象一下,这个过程已经运行了一段时间.有可能该进程的许多内存页面已被交换到磁盘.至少他们不再处于L1或L2缓存中.在这种情况下,卸载的应用程序没有必要将所有这些数据和代码页交换回内存,以"释放"当进程终止时将由操作系统释放的资源.这适用于托管甚至某些非托管资源.只有处理非后台线程保持活动的资源才能处理,否则进程将保持活动状态.
现在,在正常执行期间,必须正确清理短暂的资源(因为@fezmonkey指出数据库连接,套接字,窗口句柄)以避免非托管内存泄漏.这些是必须处理的事物.如果你创建了一个拥有一个线程的类(并且我自己的意思是它创建它并因此负责确保它停止,至少通过我的编码风格),那么该类很可能必须实现IDisposable
并拆除线程Dispose
.
.NET框架使用该IDisposable
接口作为信号,甚至警告开发人员必须处理此类.我想不出框架中实现IDisposable
(排除显式接口实现)的任何类型,其中dispos是可选的.
如果要立即删除,请使用非托管内存.
看到:
Marshal.AllocHGlobal
Marshal.FreeHGlobal
Marshal.DestroyStructure
在您发布的示例中,它仍然不"立即释放内存".所有内存都是垃圾回收,但它可能允许在前一代收集内存.你必须运行一些测试才能确定.
框架设计指南是指南,而不是规则.它们告诉您接口主要用于什么,何时使用它,如何使用它以及何时不使用它.
我曾经使用IDisposable读取了一个简单的RollBack()故障代码.下面的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来创建更干净的代码.
我不会重复关于使用或释放未受管理资源的常规内容,这些内容已全部涵盖.但我想指出似乎是一种常见的误解.
给出以下代码
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,就没有什么能阻止GC重新声明_Large正在使用的内存,即使LargeStuff的实例可能仍然在范围内._Large中的字符串也可能在gen 0中,但LargeStuff的实例可能是gen 2,因此,内存将更快地被重新声明.
添加终结器来调用上面显示的Dispose方法没有意义.这将延迟重新声明内存以允许终结者运行.
除了其主要用途是控制系统资源生存期的一种方法(完全由Ian的真棒答案所覆盖!)之外,IDisposable / using组合还可以用于确定(关键)全局资源的状态变化:在控制台中,线程的过程中,任何全局对象就像一个应用实例。
我已经写了一篇关于这种模式的文章:http : //pragmateek.com/c-scope-your-global-state-changes-with-idisposable-and-the-using-statement/
它说明了如何以可重用和可读的方式保护某些经常使用的全局状态:控制台颜色,当前线程区域性,Excel应用程序对象属性 ...