C#中IDispose接口的实现及为何这么实现详解
前言
我原本认为对于IDispose的实现方法,只要在里面释放非托管资源就行了,但是通过网上资料,看到很多实现方法并不是仅仅做释放非托管资源,非常迷惑,关键是这些资料也没详细的告诉你为什么这么做?之后通过StackOverflow了解到这一步一步的原因,说的十分详细,结合自己的认识,翻译后分享给大家:
一、IDispose的实现方法
具体的实现方法,你可以直接查看这个海外IDC网网站的教程:
https://www.jb51.net/article/54899.htm
如果你能看懂,并且很清楚为什么那么做。那么以下的文章你就可以略去不看。如果不清楚为什么那么做,请带着你的迷惑往下看:
二、为什么那样实现
英文好的可以直接去StackOverflow原文地址:
https://stackoverflow.com/questions/538060/proper-use-of-the-idisposable-interface/538238#538238
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【来源:http://www.nextecloud.cn/hk.html 转载请保留连接】! }
但是这有一个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垃圾回收的时间,提高效率。何乐而不为呢?
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对海外IDC网的支持。