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()
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
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()
{
Dispose();
}
但是这段代码中有一个错误。你看,垃圾回收器在一个后台线程上运行;你不知道两个对象被销毁的顺序。完全有可能在你的Dispose()
代码中,你想要处理的托管对象已经不存在了(因为你想要帮忙清理):
public void Dispose()
{
Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose();
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose();
this.frameBufferImage = null;
}
}
所以你需要的是一种方法,让Finalize()
告诉Dispose()
它不应该触及任何托管资源(因为它们可能已经不存在了),同时释放非托管资源。
标准模式是让Finalize()
和Dispose()
都调用一个第三个方法;在这个方法中,你传递一个布尔值,表示你是从Dispose()
(而不是Finalize()
)调用它,这意味着可以安全地释放托管资源。
这个内部方法可以被赋予任意名称,比如"CoreDispose"或"MyInternalDispose",但是惯例上称之为Dispose(Boolean)
:
protected void Dispose(Boolean disposing)
但是一个更有帮助的参数名称可能是:
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
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);
}
并将您的终结器设置为:
~MyObject()
{
Dispose(false);
}
注意:如果您的对象是从实现Dispose的对象继承而来的,请在重写Dispose时不要忘记调用它们的基Dispose方法。
public override void Dispose()
{
try
{
Dispose(true);
}
finally
{
base.Dispose();
}
}
一切都很好,但你可以做得更好!
如果用户在您的对象上调用
Dispose()
,那么一切都已经清理干净了。稍后,当垃圾收集器过来并调用Finalize时,它将再次调用
Dispose
。
这不仅浪费资源,而且如果您的对象对您已经从上一次
Dispose()
中处理的对象有垃圾引用,那么您将尝试再次处理它们!
您会注意到,在我的代码中,我小心地删除了我已处理的对象的引用,因此我不会尝试在垃圾对象引用上调用
Dispose
。但这并没有阻止一个微妙的错误悄然而至。
当用户调用
Dispose()
时:句柄
CursorFileBitmapIconServiceHandle被销毁。稍后,当垃圾收集器运行时,它将尝试再次销毁相同的句柄。
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
...
}
你可以通过告诉垃圾回收器无需对该对象进行终结来解决这个问题——它的资源已经被清理干净,不需要再做更多的工作。在
Dispose()
方法中调用
GC.SuppressFinalize()
即可实现此功能。
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
现在用户已经调用了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
清理未受管制资源的优点;你可以知道并控制未受管制资源何时被清理。它们的销毁是“确定性”的。
回答你最初的问题:为什么不现在释放内存,而是等到垃圾回收器决定释放呢?我有一个面部识别软件,需要立即清除掉530 MB的内部图像,因为它们不再需要。如果我们不这样做:机器就会陷入交换停顿。
额外阅读
对于喜欢这种解释为什么,以便如何变得明显的人,我建议您阅读唐·博克斯(Don Box)的《Essential COM》第一章:
在35页中,他解释了使用二进制对象的问题,并在你的眼前发明了COM。一旦你意识到COM的为什么,剩下的300页就很明显了,只是详细介绍了Microsoft的实现。
我认为每个曾经处理过对象或COM的程序员,至少应该阅读第一章。这是有史以来最好的解释。
额外奖励阅读
当你所知道的一切都是错的 存档作者:Eric Lippert
因此,编写正确的终结器非常困难,我能给你的最好建议就是不要尝试。
IDisposable
并不会标记任何东西。Dispose
方法会执行必要的操作,以清理实例使用的资源。这与垃圾回收无关。 - John SaundersIDisposable
。这就是为什么我说被接受的答案没有回答OP想要问的问题(以及后续编辑),即IDisposable
是否有助于<i>释放内存</i>。由于IDisposable
与释放资源而不是释放内存无关,因此像你所说的,根本没有必要将托管引用设置为null,这正是OP在他的示例中所做的。因此,对他的问题的正确答案是“不,它并不能更快地释放内存,事实上,它根本不能释放内存,只能释放资源”。但无论如何,感谢您的建议。 - Punit Vora