C#中IDispose接⼝的实现及为何这么实现详解
前⾔
我原本认为对于IDispose的实现⽅法,只要在⾥⾯释放⾮托管资源就⾏了,但是通过⽹上资料,看到很多实现⽅法并不是仅仅做释放⾮托管资源,⾮常迷惑,关键是这些资料也没详细的告诉你为什么这么做?之后通过StackOverflow了解到这⼀步⼀步的原因,说的⼗分详细,结合⾃⼰的认识,翻译后分享给⼤家:
⼀、IDispose的实现⽅法
具体的实现⽅法,你可以直接查看这个⽹站的教程:
如果你能看懂,并且很清楚为什么那么做。那么以下的⽂章你就可以略去不看。如果不清楚为什么那么做,请带着你的迷惑往下看:
⼆、为什么那样实现
英⽂好的可以直接去StackOverflow原⽂地址:
2.1、进⾏之前
在C++中,所有你在堆上申请的内存空间,必须⼿动释放掉,否则就会造成内存的泄露。这可能会让你在写程序的时候要花点⼼思在内存的管理上⽽不是专注于解决你编程的⽬的—解决问题。所以作为C++的进化版C#使⽤了GC(Garbage Collector)来进⾏内存的管理以达到⾃动释放不需要的内存的⽬的,但是GC并不能做的⼗分完美,对于⼀些⾮托管资源,GC⽆能为⼒,这就要求我们必须⼿动的释放那么⾮托管资源,为了更好的去做到这⼀点,我们就要编写⼀种⽅法,通过⼿动调⽤这个⽅法,我们就能够释放掉⾮托管资源。
注:
什么是托管资源和⾮托管资源?
托管资源就是托管给CLR的资源,CLR能对这些资源进⾏管理。⽽⾮托管资源则是CLR⽆法对这些资源管理,这些资源的申请、释放必须由使⽤者⾃⾏管理。
例如,像Win32编程中的⽂件句柄,上下⽂句柄、窗⼝或⽹络连接等资源都属于⾮托管资源。但是如果这些⾮托管资源在.Net 中进⾏了封装,成为了.Net类库中的⼀部分,它就不属于⾮托管资源了,因为在对它们封装的过程中,就实现了它们的⾃动管理功能。
也就是说,你能在.Net中到的类产⽣的对象,都是托管资源。
(理解这点很重要,这可能是你看不懂上⾯实现教程的重要⼀个原因!)
注:
GC进⾏垃圾回收的时间和顺序?
GC进⾏垃圾回收的时间我们根本⽆法确定(当然你⼿动调⽤GC的垃圾回收⽅法除外),并且顺序也不能确定!也就是说,你先申请的空间有可能在你后申请的空间释放之后释放。
GC对于实现析构函数和没实现析构函数的类处理⽅法不⼀样,简单些说GC对于实现了析构函数的类⼀定会调⽤他们的析构函数。
关于.Net的垃圾回收机制,你可以暂时先知道这么多,待看完了这篇⽂章再去深⼊了解。
2.2、我们需要编写⼀种⽅法去释放!
为了去清除⼀些⾮托管资源,你创建的类需要有⼀个public⽅法,⽅法的名字可以随意命名
例如:
public void Cleanup()
public void Shutdown()
……
你可以这么做,但是有⼀个标准的名字
public void Dispose()
甚⾄有⼀个接⼝IDisposeable,⾥⾯包含的就是刚才那个⽅法
public interface IDisposable
{
void Dispose()
}
因此最好的办法是让你的类去实现IDisposable接⼝,在接⼝内的Dispose⽅法内提供⼀段清除⾮托管资源的代码。
public void Dispose()
{
//这⾥释放⼀个句柄(句柄是⼀个⾮托管资源,属于Win32编程的概念)
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}
OK。这就完成了,除⾮你想做的更好!
2.3、别忘了类中的托管资源还占着空间!
托管资源占着空间?你⾸先想到的可能是那些int,string等等这些托管资源,它们能占⽤⼏个空间,他们占着就占着呗!
但是托管资源可不仅仅是那些资源,要是你的对象使⽤了250MB的System.Drawing,Bitmap(这是在.Net Frame中的,属于托管资源)作为⼀些缓冲怎么办?当然,你知道这是⼀个.Net的托管资源,所以GC理所应当的将会释放它。但是你真的想留着250MB的内存空间就那么被占⽤着?然后等待着GC最终释放它?更或者要是有⼀个更⼤数据库连接呢?我们当然不想让那连接⽩⽩占⽤来等待GC的终结!
如果⽤户调⽤了Dispose⽅法(意味着他们不再想使⽤这个对象⾥的⼀切)为什么不去扔掉那些浪费空间的位图资源和数据库连接呢?
那么,我们就应该这么做:
释放⾮托管资源(因为我们必须这么做)
释放托管资源(让你的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;
}
}
OK,做的很好了,除⾮你想做的更好!
2.4、总会有⼈粗⼼忘记调⽤Dispose!
要是有⼈使⽤你的类创建了对象,但是忘记调⽤Dispose⽅法该怎么办?这将会泄露⼀些⾮托管的资源!
注意:忘记调⽤Dispose⽅法虽然会造成⾮托管资源的泄露,但是对于那些托管资源来说,是不会泄露的,因为最终GC会⾏动起来,在后台线程中释放那些和托管资源有关的内存空间。这包括你创建的对象和其中的托管资源(例如Bitmap和数据库连接) (为什么?往下看)
也就是说,如果你忘记调⽤Dispose⽅法,这个类应该⾃动进⾏⼀些补救措施!我们可以想到设计⼀种⽅法来做为⼀种后备⽅法:利⽤GC最终调⽤的终结器
注意:GC最终虽然会释放托管资源,但是GC并不知道或者关⼼你的Dispose⽅法。那仅仅是⼀个我们选择的名字。
GC调⽤析构函数是⼀个完美的时机来释放托管资源,我们实现析构函数的功能通过重写Finalize⽅法。
注意:在C#中,你不能真的去使⽤重写虚函数的⽅法去重写Finalize⽅法。你只能使⽤像C++的析构函数的语法去编写,编译
器会⾃动对你的代码进⾏⼀些改动来实现override终结器⽅法。(具体可查看MSDN中析构函数⼀节)
~MyObject()
{
//we're being finalized (i.e.destroyed), call Dispose in case the user forgot to
Dispose(); //<--Warning:subtle bug! Keep reading!
}
但是这有⼀个Bug。
试想⼀下,你的GC是在后台线程调⽤,你对GC的调⽤时间和对垃圾对象的释放顺序根本⽆法掌控,很有可能的发⽣的是,你的对象的某些托管资源已经在调⽤终结器之前就被释放了,当GC调⽤终结器时,最终会释放两次托管资源!
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
//Free managed resources too
if (this.databaseConnection !=null)
{
this.databaseConnection.Dispose(); //<-- crash, GC already destroyedit
this.databaseConnection =null;
}
if (this.frameBufferImage !=null)
{
this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
this.frameBufferImage = null;
}
}
所以你需要做的是让终结器告诉Dispose⽅法,它不应该再次释放任何托管资源了(因为这些资源很有可能已经被释放了!)
标准的Dispose模式是让Dispose函数和Finalize⽅法通过调⽤第三种⽅法,这个⽅法有⼀个bool类型的形参来表⽰此⽅法的调⽤是通过Dispose还是GC调⽤终结器。在这个⽅法⾥实现具体的释放资源代码。
这个第三种⽅法的命名当然可以随意命名,但是规范的⽅法签名是:
protected void Dispose(Boolean disposing)
当然,这个参数的名字会让⼈读不懂什么意思。(很多⽹上教程都使⽤这个名字,第⼀次看很难看懂这个变量的作⽤)
这个参数也许可以这样写…
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//Free managed resources too, butonly if I'm being called from Dispose
//(If I'm being called fromFinalize 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 youfrom Dispose, it's safe
}
终结器就变成了这样…
~MyObject()
{
Dispose(false); //I am *not*calling you from Dispose, it's *not* safe
}
注意:你的类如果是从另⼀个类继承⽽来,那么你不要忘记去调⽤⽗类的Dispose⽅法
public Dispose()
{
try
{
Dispose(true); //true: safeto free managed resources
}
finally
{
base.Dispose();
}
}
所有的⼀切看起来都已经很好了,除⾮,你想做的更好!
2.5、最完美的⽅法?
如果使⽤者⼿动调⽤了Dispose⽅法,这样,⼀切都被清空了。之后呢,因为你重写了Finalize⽅法,GC⼀定调⽤这个⽅法,它将会再⼀次调⽤Dispose⽅法!
这不仅是⼀种性能上的浪费,⽽且关键是在Dispose⽅法调⽤后,那些引⽤已经变成了垃圾,GC会调⽤这些垃圾引⽤!
解决的⽅法是通过在Dispose⽅法中调⽤GC.SuppressFinalize() 来阻⽌GC去调⽤Finalize⽅法
public void Dispose()
析构方法
{
Dispose(true); //I am calling youfrom Dispose, it's safe
GC.SuppressFinalize(this); //Hey,GC: don't bother calling finalize later
}
这样,每⼀件事都照顾到了!
(注:其实可以将Dispose(bool disposing)⽅法变成虚函数,如果你的类被继承)
⾄此,我们⼀步⼀步实现了最好的IDisposable⽅法,现在回头去看看⼀开始的实现IDisposable接⼝教程,是不是⼀切的透彻了?
三、使⽤终结器还是Dispose⽅法释放⾮托管资源?
其实两种⽅法都可以,但是就像在⼀开始提到的,GC的垃圾回收时间不确定,对于那些你已经不需要的资源,还是尽快释放⽐较好,不应该总等着GC的垃圾回收,⽽且还有⼀个好处是,降低GC垃圾回收的时间,提⾼效率。何乐⽽不为呢?
总结
以上就是这篇⽂章的全部内容了,希望本⽂的内容对⼤家的学习或者⼯作具有⼀定的参考学习价值,如果有疑问⼤家可以留⾔交流,谢谢⼤家对的⽀持。