1. 什麼是IDisposable? IDisposable介面是一個用於約定可進行釋放資源操作的介面,一個類實現該介面則意味著可以使用介面約定的方法Dispose來釋放資源。其定義如下: public interface IDisposable { void Dispose(); } 上述描述中可 ...
1. 什麼是IDisposable?
IDisposable
介面是一個用於約定可進行釋放資源操作的介面,一個類實現該介面則意味著可以使用介面約定的方法Dispose
來釋放資源。其定義如下:
public interface IDisposable
{
void Dispose();
}
上述描述中可能存在兩個問題:
1. 什麼是“資源”?
2. C#是一個運行在含有垃圾回收(GC)平臺上的語言,為什麼還需要手動釋放資源?
1.1 資源
資源包括托管資源和非托管資源,具體來說,它可以是一個普通的類實例,是一個資料庫連接,是一個文件指針,或者是一個視窗的句柄等等。不太準確地說,你可以理解為就是程式運行時用到的各種東西。1.2 為什麼要手動釋放資源
對於托管資源,通常來說由於CLR的GC的幫助,可以自動釋放回收而無需程式員手動管理。然而,由於C#允許使用非托管資源,這些非托管資源不受GC的控制,無法自動釋放回收,因此對於這類資源,就要程式員進行手動管理。另一方面,有些資源雖然是托管資源,但是實際包裝了一個非托管資源,並實現了IDispose
介面,同樣的,對於這類資源,最好也手動管理。
(ps:CLR,Common Language Runtime,即C#編譯後的IL代碼的運行平臺)
(ps:GC,Garbage Collection,垃圾回收,即一種用於自動回收資源的機制)
如果你寫過C++,這就相當於應該在實例銷毀時釋放掉成“new”出來的分配到堆上的資源,否則資源將一直保留在記憶體中無法釋放,導致記憶體泄漏等一系列問題。
在C++中,通常將資源釋放的操作放置在類的析構函數中,但C#並沒有析構函數這一概念,因此,C#使用IDisposable
介面來對資源釋放做出約定——當程式員看到一個類實現IDisposable
介面時,就應該想到在使用完該類的實例後就應該調用其Dispose
方法來及時釋放資源。
對於實現了IDispose
介面的類,在C#中你通常可以採用如下方式來釋放資源:
1:try...finally
UnmanagedResource resource = /* ... */;
try
{
// 各種操作
}
finally
{
resource.Dispose();
}
(註:在finally中釋放是為了確保即便運行時出錯也可以順利釋放資源)
2:using
using (UnmanagedResource resource = /* ... */)
{
// 離開using的作用域後會自動調用resource的Dispose方法
}
// 或者如果不需要額外控製作用域的簡寫
using UnmanagedResource resource = /* ... */;
(ps:實際上,哪怕不實現IDisposable介面,只要類實現了public void Dispose()方法都可以使用using進行管理)
(ps:using本質上是try...finally的語法糖,所以即便using塊中拋出異常也可以正常釋放資源)
2. 如何實現IDisposable
2.1 不太完美的基本實現
你可能還會認為IDisposable
很容易實現,畢竟它只有一個方法需要實現,並且看上去只要在方法里釋放掉需要釋放的資源即可:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
// 釋放需要釋放的資源
}
}
通常來說這樣做也不會有什麼大問題,然而,有幾個問題需要考慮。接下來將逐步闡述問題並給出解決方案。
2.2 如果使用者忘記了調用Dispose方法釋放資源
儘管程式員都應該足夠細心來保證他們對那些實現了Disposable
介面的類的實例調用Dispose
方法,但是,出於各種原因,或許是他是一名新手,或許他受到老闆的催促,或許他昨天沒睡好等等,這些都可能導致他沒有仔細檢查自己的代碼。
永遠不要假設你的代碼會被一直正確地使用,總得留下些兜底的東西,提高健壯性——把你的用戶當做一個做著布朗運動的白痴,哪怕他可能是個經驗豐富的程式員,甚至你自己。對於這樣的問題,最自然的想法自然是交給GC來完成——如果程式員忘記了調用
Dispose
方法釋放資源,就留著讓GC來調用釋放。還好,C#允許你讓GC來幫助你調用一些方法——通過終結器。
關於終結器的主題會是一個比較複雜的主題,因此在這裡不展開討論,將更多的細節留給其他主題。就本文而言,暫時只需要知道終結器的聲明方法以及GC會在“某一時刻”自動調用終結器即可。(你或許想問這個“某一時刻”是什麼時候,這實際上是需要交給複雜主題來討論的話題)
聲明一個終結器類似於聲明一個構造方法,但是需要在方法的類名前添加一個~
。如下:
class UnmanagedResource : IDisposable
{
// UnmanagedResource的終結器
~UnmanagedResource()
{
// 一些操作
}
}
關於終結器,下麵是一些你需要知道的:
1:一個類中只能定義一個終結器,且終結器不能有任何訪問修飾符(即不能添加public/private/protected/internal)
2:永遠不要手動調用終結器(實際上你也無法這麼做)
由於GC會在某一個時刻自動調用終結器,因此如果在終結器中調用Dispose方法,即使有粗心的程式員忘記了手動釋放資源,GC也會在某一時刻來幫他們兜底。如下:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
// 釋放需要釋放的資源
}
~UnmanagedResource()
{
// 終結器調用Dispose釋放資源
Dispose();
}
}
(ps:你或許會覺得終結器很像C++的析構函數,無論是聲明方式還是作用(釋放資源)上,但是終結器和析構函數有本質上差別,但這裡不展開討論)
2.3 手動調用了Dispose後,終結器再次調用Dispose
當你手動調用了Dispose
方法後,並不表示你就告訴了GC不要再調用它的終結器,實際上,在你調用Dispose
方法後,GC還是會在某一時刻調用終結器,而由於我們在終結器里調用了Dispose
方法,這會導致Dispose
方法再次被調用——Double Free!
當然,要解決這一問題非常簡單,只需要用一個欄位來表明資源是否被釋放,併在Dispose
方法里檢查這個欄位的值,一旦發現已經釋放則過就立刻返回。如下:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
// 如果已經釋放過就立刻返回
if (_disposed)
{
return;
}
// 釋放需要釋放的資源
// 標記已釋放
_disposed = true;
}
~UnmanagedResource()
{
Dispose();
}
// 用於標記是否已經釋放的欄位
private bool _disposed;
}
這樣可以解決資源被重覆釋放的問題,但是這還是無法阻止GC調用終結器。當然你或許會認為讓GC調用終結器沒什麼問題,畢竟我們保證了Dispose
重覆調用是安全的。不過,要知道終結器是會影響性能的,因此為了性能考慮,我們還是希望在Dispose
方法調用後阻止終結器的執行(畢竟這時候已經不需要GC兜底了)。而要實現這一目標十分簡單,只需要在Dipose
方法中使用GC.SuppressFinalize(this)
告訴GC不要調用終結器即可。如下:
class UnmanagedResource : IDisposable
{
public void Dispose()
{
if (_disposed)
{
return;
}
// 釋放需要釋放的資源
_disposed = true;
// 告訴GC不要調用當前實例(this)的終結器
GC.SuppressFinalize(this);
}
~UnmanagedResource()
{
Dispose();
}
private bool _disposed;
}
這樣,如果調用了Dispose
方法,就會“抑制”GC對終結器的調用;而讓終結器調用Dispose
也不會產生什麼問題。
2.4 不是任何時候都需要釋放所有資源
考慮一個比較複雜的類:class UnmanagedResource : IDisposable
{
// 其他代碼
private FileStream _fileStream;
}
上述例子中,FileStream
是一個實現了IDisposable
的類,也就是說,FileStream
也需要進行釋放。UnmanagedResource
不僅要釋放自己的非托管資源,還要釋放FileStream
。你或許認為只需要在UnmanagedResource
的Dispose
方法中調用一下FileStream
的Dispose
方法就行。如下:
class UnmanagedResource : IDisposable
{
// 其它代碼
public void Dispose()
{
// 其他代碼
_fileStream.Dispose();
// 其它代碼
}
private FileStream _fileStream;
}
咋一看沒什麼問題,但是考慮一下,如果UnmanagedResource
的Dispose
方法是由終結器調用的會發生什麼?
提示:終結器的調用是無序的。
是的,很可能FileStream
的終結器先被調用了,執行過了其Dispose
方法釋放資源,隨後UnmanagedResource
的終結器調用Dispose
方法時會再次調用FileStream
的Dispose
方法——Double Free, Again。
因此,如果Dispose
方法是由終結器調用的,就不應該手動釋放那些本身就實現了終結器的托管資源——這些資源的終結器很可能先被執行。僅當手動調用Dispose
方法時才手動釋放那些實現了終結器的托管資源。
我們可以使用一個帶參數的Dispose
方法,用一個參數來指示Dispose
是否釋放托管資源。稍作調整,實現如下:
class UnmanagedResource : IDisposable
{
// 其它代碼
private void Dispose(bool disposing)
{
// 其他代碼
if (disposing)
{
// 釋放托管資源
_fileStream.Dispose();
}
// 釋放非托管資源
// 其它代碼
}
}
上述代碼聲明瞭一個接受disposing
參數的Dispose(bool disposing)
方法,當disposing
為true
時,同時釋放托管資源和非托管資源;當disposing
為false
時,僅釋放托管資源。另外,為了不公開不必要的介面,將其聲明為private
。
接下來,只需要在Dispose
方法和終結器中按需調用Dispose(bool disposing)
方法即可。
class UnmanagedResource : IDisposable
{
// 其它代碼
public void Dispose()
{
// disposing=true,手動釋放托管資源
Dispose(true);
GC.SuppressFinalize(this);
}
~UnmanagedResource()
{
// disposing=false,不釋放托管資源,交給終結器釋放
Dispose(false);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// 釋放托管資源
}
// 釋放非托管資源
_disposed = true;
}
}
2.5 考慮一下子類的資源釋放
考慮一下如果有UnmanagedResource
的子類:
class HandleResource : UnmanagedResource
{
private HandlePtr _handlePtr;
}
HandleResource
有自己的資源HandlePtr
,顯然如果只是簡單繼承UnmanagedResource
的話,UnmanagedResource
的Dispose
方法並不能釋放HandleResource
的HandlePtr
。
那麼怎麼辦呢?使用多態,將UnmanagedResource
的Dispose
方法聲明為virtual
併在HandleResource
里覆寫;或者在HandleResource
里使用new
重新實現Dispose
似乎都可以:
// 使用多態
class UnmanagedResource : IDisposable
{
public virtual void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
public override void Dispose() { /* ... */}
}
// 重新實現
class UnmanagedResource : IDisposable
{
public void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
public new void Dispose() { /* ... */}
}
這兩種方法似乎都可行,但是一個很大的問題是,你還得對HandleResource
重覆做那些在它的父類UnmanagedResource
做過的事——解決重覆釋放、定義終結器以及區分對待托管和非托管資源。
這太不“繼承了”——顯然,有更好的實現方法。
答案是:將UnmanagedResource
的的Dispose(bool disposing)
方法訪問許可權更改為protected
,並修飾為virtual
,以讓子類訪問/覆蓋:
class UnmanagedResource : IDisposable
{
protected virtual void Dispose(bool disposing) { /* ... */ }
}
這樣,子類可以通過覆寫Dispose(bool disposing)
來實現自己想要的釋放功能:class HandleResource : UnmanagedResource
{
protected override void Dispose(bool disposing)
{
// 其他代碼
base.Dispose(disposing);
}
}
(ps:建議先釋放子類資源,再釋放父類資源)
由於Dispose(bool disposing)
是虛方法,因此父類UnmanagedResource
的終結器和Dispose
方法中對Dispose(bool disposing)
的調用會受多態的影響,調用到正確的釋放方法,故子類可以不必再做那些重覆工作。
3. 總結
3.1 代碼總覽
class UnmanagedResource : IDisposable
{
// 對IDisposable介面的實現
public void Dispose()
{
// 調用Dispose(true),同時釋放托管資源與非托管資源
Dispose(true);
// 讓GC不要調用終結器
GC.SuppressFinalize(this);
}
// UnmanagedResource的終結器
~UnmanagedResource()
{
// 調用Dispose(false),僅釋放非托管資源,托管資源交給GC處理
Dispose(false);
}
// 釋放非托管資源,並可以選擇性釋放托管資源,且可以讓子類覆寫的Dispose(bool disposing)方法
protected virtual void Dispose(bool disposing)
{
// 防止重覆釋放
if (_disposed)
{
return;
}
// disposing指示是否是否托管資源
if (disposing)
{
// 釋放托管資源
}
// 釋放非托管資源
// 標記已釋放
_disposed = true;
}
}
參考資料/更多資料:
【1】:IDisposable 介面
【2】:實現 Dispose 方法