正確實作IDisposable
.NET中用來釋放物件資源的介面是IDisposable,但是這個介面的實作還是比較有講究的,此外還有Finalize和Close兩個函數。
MSDN建議依照下面的模式實作IDisposable介面:
1 public class Foo: IDisposable
2 {
3 public void Dispose()
4 {
5 Dispose(true);
6 GC.SuppressFinalize(this);
7 }
8
9 protected virtual void Dispose(bool disposing)
10 {
11 if (!m_disposed)
12 {
13 if (disposing)
14 {
15 // Release managed resources
16 }
17
18 // Release unmanaged resources
19
20 m_disposed = true;
21 }
22 }
23
24 ~Foo()
25 {
26 Dispose(false);
27 }
28
29 private bool m_disposed;
30 }
31
32
在.NET的物件中實際上有兩個用於釋放資源的函數:Dispose和Finalize。 Finalize的目的是用於釋放非託管的資源,而Dispose是用於釋放所有資源,包括託管的和非託管的。
在這個模式中,void Dispose(bool disposing)函數透過一個disposing參數來區別目前是否是被Dispose()呼叫。如果是被Dispose()調用,那麼需要同時釋放託管和非託管的資源。如果是被~Foo()(也就是C#的Finalize())呼叫了,那麼只需要釋放非託管的資源即可。
這是因為,Dispose()函數是被其它程式碼明確地呼叫並要求釋放資源的,而Finalize是被GC呼叫的。在GC呼叫的時候Foo所引用的其它託管物件可能還不需要被銷毀,並且即使要銷毀,也會由GC來呼叫。因此在Finalize中只需要釋放非託管資源即可。另外一方面,由於在Dispose()中已經釋放了託管和非託管的資源,因此在物件被GC回收時再次呼叫Finalize是沒有必要的,所以在Dispose()中呼叫GC.SuppressFinalize(this)避免重複調用Finalize。
然而,即使重複呼叫Finalize和Dispose也是不存在問題的,因為有變數m_disposed的存在,資源只會被釋放一次,多餘的呼叫會被忽略過去。
因此,上面的模式保證了:
1、 Finalize只釋放非託管資源;
2, Dispose 釋放託管和非託管資源;
3、 重複呼叫 Finalize和Dispose是沒有問題的;
4、Finalize和Dispose共享相同的資源釋放策略,因此他們之間也是沒有衝突的。
在C#中,這個模式需要明確地實現,其中C#的~Foo()函數代表了Finalize()。而在C++/CLI中,這個模式是自動實現的,C++的類別析構函數則是不一樣的。
依照C++語義,析構函數在超出作用域,或是delete的時候被呼叫。在Managed C++(即.NET 1.1中的託管C++)中,析構函數相當於CLR中的Finalize()方法,在垃圾收集的時候由GC調用,因此,呼叫的時機是不明確的。在.NET 2.0的C++/CLI中,析構函數的語意被修改為等價與Dispose()方法,這就隱含了兩件事情:
1、 所有的C++/CLI中的CLR類別都實作了介面IDisposable,因此在C#中可以用using關鍵字來存取這個類別的實例。
2、 析構函數不再等價於Finalize()了。
對於第一點,這是一件好事,我認為在語意上Dispose()更接近C++析構函式。對於第二點,Microsoft進行了一次擴展,做法是引入了「!」函數,如下所示:
1 public ref class Foo
2 {
3 public:
4 Foo();
5 ~Foo(); // destructor
6 !Foo(); // finalizer
7 };
8
「!」函數(我實在不知道該怎麼稱呼它)取代原來Managed C++中的Finalize()被GC呼叫。 MSDN建議,為了減少程式碼的重複,可以寫這樣的程式碼:
1 ~Foo()
2 {
3 //釋放託管的資源
4 this->!Foo();
5 }
6
7 !Foo()
8 {
9 //釋放非託管的資源
10 }
11
對於上面這個類,實際上C++/CLI產生對應的C#程式碼是這樣的:
1 public class Foo
2 {
3 private void !Foo()
4 {
5 // 釋放非託管的資源
6 }
7
8 private void ~Foo()
9 {
10 // 釋放託管的資源
11 !Foo();
12 }
13
14 public Foo()
15 {
16 }
17
18 public void Dispose()
19 {
20 Dispose(true);
21 GC.SuppressFinalize(this);
22 }
23
24 protected virtual void Dispose(bool disposing)
25 {
26 if (disposing)
27 {
28 ~Foo();
29 }
30 else
31 {
32 try
33 {
34 !Foo();
35 }
36 finally
37 {
38 base.Finalize();
39 }
40 }
41 }
42
43 protected void Finalize()
44 {
45 Dispose(false);
46 }
47 }
48
由於~Foo()和!Foo()不會被重複調用(至少MS這樣認為),因此在這段程式碼中沒有和前面m_disposed相同的變量,但是基本的結構是一樣的。
並且,可以看到實際上並不是~Foo()和!Foo()就是Dispose和Finalize,而是C++/CLI編譯器產生了兩個Dispose和Finalize函數,並在適當的時候呼叫它們。 C++/CLI其實已經做了很多工作,但唯一的一個問題就是依賴使用者在~Foo()中呼叫!Foo()。
關於資源釋放,最後一點要提的是Close函數。在語意上它和Dispose很類似,依照MSDN的說法,提供這個函數是為了讓使用者感覺舒服一點,因為對於某些對象,例如文件,使用者更習慣呼叫Close()。
然而,畢竟這兩個函數做的是同一件事,因此MSDN建議的程式碼就是:
1 public void Close()
2 {
3 Dispose(();
4 }
5
6
這裡直接呼叫不帶參數的Dispose函數以獲得和Dispose相同的語意。這似乎就圓滿了,但從另一方面說,如果同時提供了Dispose和Close,會給用戶帶來一些困惑。沒有看到程式碼細節的前提下,很難知道這兩個函數到底有什麼差別。因此在.NET的程式碼設計規格中說,這兩個函數其實只能讓使用者用一個。因此建議的模式是:
1 public class Foo: IDisposable
2 {
3 public void Close()
4 {
5 Dispose();
6 }
7
8 void IDisposable.Dispose()
9 {
10 Dispose(true);
11 GC.SuppressFinalize(this);
12 }
13
14 protected virtual void Dispose(bool disposing)
15 {
16 // 同前
17 }
18 }
19
這裡使用了一個所謂的介面明確實作:void IDisposable.Dispose()。這個顯式實作只能透過介面來訪問,但是不能透過實作類別來存取。因此:
1 Foo foo = new Foo();
2
3 foo.Dispose(); // 錯誤
4 (foo as IDisposable).Dispose(); // 正確
5
這樣做到了兼顧兩者。對於喜歡使用Close的人,可以直接用foo.Close(),而且他看不到Dispose()。對於喜歡Dispose的,他可以把型別轉換為IDisposable 來調用,或是使用using語句。皆大歡喜!
http://www.cnblogs.com/xlshcn/archive/2007/01/16/idisposable.html