本人最近接觸一個項目,在這個項目裡面看到很多類實現了IDisposable接口.在我以前的項目中都很少用過這個接口,只知道它是用來手動釋放資源的.這麼多地方用應該有它的好處,為此自己想對它有進一步的了解,但這個過程遠沒有我想象中的簡單.
IDisposable接口定義:定義一種釋放分配的資源的方法。
.NET 平台在內存管理方面提供了GC(Garbage Collection),負責自動釋放托管資源和內存回收的工作,但它無法對非托管資源進行釋放,這時我們必須自己提供方法來釋放對象內分配的非托管資源,比如你在對象的實現代碼中使用了一個COM對象 最簡單的辦法可以通過實現Finalize()來釋放非托管資源,因為GC在釋放對象時會檢查該對象是否實現了 Finalize() 方法。 有一種更好的,那就是通過實現一個接口顯式的提供給客戶調用端手工釋放對象的方法,而不是傻傻的等著GC來釋放我們的對象.這種實現並不一定要使用了非托管資源後才用,如果你設計的類會在運行時有非常大的實例(象 GIS 中的Geometry),為了優化程序性能,你也可以通過實現該接口讓客戶調用端在確認不需要這些對象時手工釋放它們 .
在定義一個類時,可以使用兩種機制來自動釋放未托管的資源.這些機制通常放在一起實現.因為每個機制都為問題提供了略為不同的解決方法.這兩種機制是:
第一:聲明一個析構函數,作為類的一個成員.在GC回收資源時會調用.
第二:在類中實現IDisposable接口
析構函數的問題:
執行的不確定性:析構函數是由GC調用的,而GC的調用是不確定的.如果對象占用了比較重要的資源,應盡可以早的釋放資源.
IDisposable接口定義了一個模式,為釋未托管資源提供了確定的機制,並避免產生析構函數固有的與GC相關的問題.
在實際應用了,常常是結合兩種方法來取長補短.之所以要加上析構函數,是防止客戶端沒有調用Dispose方法.
本人對IDisposable接口的理解是這樣的:
這種手動釋放資源的方式肯定要比等待GC來回收要效率高啊,於是出現了下面的示例類代碼:
這個Foo類實現了IDisposable接口,裡面有一個簡單的方法:增加一個用戶.
Code
public class Foo : IDisposable { /**//// <summary> /// 實現IDisposable接口 /// </summary> public void Dispose() { Dispose(true); //.NET Framework 類庫 // GC..::.SuppressFinalize 方法 //請求系統不要調用指定對象的終結器。 GC.SuppressFinalize(this); } /**//// <summary> /// 虛方法,可供子類重寫 /// </summary> /// <param name="disposing"></param> protected virtual void Dispose(bool disposing) { if (!m_disposed) { if (disposing) { // Release managed resources } // Release unmanaged resources m_disposed = true; } } /**//// <summary> /// 析構函數 /// 當客戶端沒有顯示調用Dispose()時由GC完成資源回收功能 /// </summary> ~Foo() { Dispose(false); } /**//// <summary> /// 增加一個用戶 /// </summary> public bool AddUser() { //代碼省略 return true; } /**//// <summary> /// 是否已經被釋放過,默認是false /// </summary> public bool m_disposed; //private IntPtr handle; }
客戶端是這樣調用的:先實例化對象,然後增加一個用戶,此時銷毀對象.
Code
Foo _foo = null; _foo = new Foo(); //資源是否已經被釋放 //第一次默認為false; bool isRelease3 = _foo.m_disposed; //增加用戶 bool isAdded= _foo.AddUser(); //不再用了,釋放資源 _foo.Dispose();
C#編程的一個優點是程序員不需要擔心具體的內存管理,尤其是垃圾收集器會處理所有的內存清理工作。用戶可以得到像C++語言那樣的效率,而不需要考慮像在C++中那樣內存管理工作的復雜性。雖然不必手工管理內存,但如果要編寫高效的代碼,就仍需理解後台發生的事情。
一面運行沒有錯誤,可總想知道這個dispose方法到底做了些什麼.既然是釋放資源,那麼類被釋放後應該就被銷毀,它的引用應該是不存在的,於是本人的測試代碼如下:
Code
try { if (_foo == null) { //對象調用Dispose()後應該運行到此外 Response.Write("資源已經釋放啦!"); } else { Response.Write(_foo.GetType().ToString()); //資源是否已經被釋放 此時為true bool isRelease4 = _foo.m_disposed; bool isAdded2 = _foo.AddUser(); } } catch (Exception ex) { Response.Write("ERR"); }
本想應該會運行Response.Write("資源已經釋放啦!"),可是結果相反,它的引用依然存在.這讓我不解,後來得到園友jyk的指點,他讓我試下,.net下實現了dispose方法的類,我就用Stream試了下,測試結果好下:
Code
Stream _s = this.FileUpload1.PostedFile.InputStream; //客戶端文件大小 為了判斷對象是否被銷毀 long orgLength = _s.Length; _s.Dispose(); try { if (_s == null) { Response.Write("資源已經釋放啦!"); } else { Response.Write(_s.GetType().ToString()); //客戶端文件大小 此處為釋放資源後 //運行結果表明,此時的文件流的大小是0 //說明資源已經成功釋放 long _length= _s.Length; } } catch (Exception ex) { Response.Write("ERR"); }
運行結果我們可以非常清楚的看出,Stream資源已經被釋放,因為兩次訪問Stream的大小,發現在dispose後的大小為零.這就好像是第一次初始化的結果.但Stream屬於非托管資源,如果是托管資源呢?在Foo的測試代碼中發現,釋放前後的變量(m_disposed,調用Dispose前為false,調用後為true,而且還可以調用類的方法)發生了變化,並不是我想象當中的初始化.這是讓我一直不解的地方.
後來在資料書上看,發現IDisposable接口是專門針對未托管資源而設計的.它在托管資源上沒有特別大的幫助.最終的資源回收工作還得要GC.我們看下托管資源和非托管資源在內存上的分配情況.
/*非托管資源一般都是放在堆棧上,而托管資源都是存儲在堆上.*/ 非常感謝 Angel Lucifer的指教,本人見笑了 特此刪除:
值類型與引用類型在內存分配上的分別:
值類型存儲在堆棧中,堆棧的工作原理就是先進後出.它在釋放資源的順序上與定義變量時分配內存的順序相反.值變量一旦出了作用域就會從堆棧中刪除對象.
引用類型則存儲在堆中.,當new一個類時,此時就會為對象分配內存存入托管堆中,它可以在方法退出很長的時間後仍然可以使用.我以一句常用的實例類的語句來說明下.
classA a=new classA();
這句非常平常的語句其實可以分成兩部分來看:
第一:classA a;聲明一個classA的引用a,在堆棧上給這個引用分配存儲空間.它只是個引用,並不是真正的對象.它包含存儲對象的地址.
第二:a=new classA();分配堆上的內存,以存儲真正的對象.然後修改a的值為新對象的內存地址.
當引用出了作用域後,就會從堆棧上刪除引用,但引用對象的數據仍然存儲在托管堆中,一直到程序停止,或者是GC刪除.
所在這點就可以解釋我上面寫的Foo類在調用Dispose方法後,程序仍然可以訪問對象的原因了.
/*我認為堆是否就有像asp.net中的緩存功能,它可以將對象緩存起來,對象只要創建一次就可以在一定的有限時間內存在.*/
非常感謝 Angel Lucifer的指教 特此更正如下:
這種情況完全是因為GC回收操作的不可預測性導致的。GC Heap上的對象生存期完全看GC是否要回收它而決定。此外,值類型完全沒必要實現 IDisposable 接口。
總結:
如果你的類中沒有用非托管資源,或者是非常大的實例(象 GIS 中的Geometry), 就沒有太大的必要實現這個接口. 並不是實現了這樣的接口就說明你寫的類有多大的不同或者會帶來多大的性能優勢.