Dispose的目的是释放非托管资源。这需要在某个时候完成,否则它们永远不会被清理。垃圾回收器不知道如何对IntPtr类型的变量调用DeleteHandle(),也不知道是否需要调用DeleteHandler()。
注意:什么是非托管资源?如果您在Microsoft.NET Framework中找到它:它是托管的。如果你自己去打听MSDN,它是不受管理的。任何您使用P/Invoke调用来脱离.NET Framework中所有可用内容的舒适世界的东西都是非托管的,现在您负责清理它。
您创建的对象需要公开一些外部世界可以调用的方法,以便清理非托管资源。该方法可以任意命名:
public void Cleanup()
or
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。但这并没有阻止一个微妙的bug潜入。
当用户调用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的第一章:
直接链接:皮尔逊出版公司第1章样本磁铁:84bf0b960936d677190a2be355858e80ef7542c0
在35页中,他解释了使用二进制对象的问题,并在你眼前发明了COM。一旦你意识到COM的原因,剩下的300页就显而易见了,只需详细介绍一下微软的实现。
我认为每一个处理过对象或COM的程序员至少都应该阅读第一章。这是对任何事情最好的解释。
额外奖励阅读
当你所知道的一切都是错误的
因此,确实很难编写正确的终结器,我能给你的最好建议是不要尝试。