在前幾個做法都有提到 IDisposable 的相關概念,這個做法就是在討論如何寫出標準的 Dispose 模式。
之前也有提到實做 IDisposable
介面時使用者需要自行注意釋放的時機,因此最方便的做法就是搭配 using
這樣內部程式碼結束後會自動呼叫
Dispose
方法,為了避免使用者忘記釋放掉記憶體或不知道要使用 using
來釋放,我們在實做的時候可以選擇將 finalizer
與 IDisposable
結合起來的寫法
,這樣即可以手動釋放也可以避免使用者忘記的情況發生。
以下是實做 IDisposable
介面通常會實做的方法,這個也算是一種常用的設計模式叫做 Dispose Pattern
public class DisposableResourceHolder : IDisposable {
bool _disposed = false;
private SafeHandle resource; // handle to a resource
public DisposableResourceHolder() {
this.resource = ... // allocates the resource
}
// ~DisposableResourceHolder() {
// Dispose(false);
// }
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (_disposed)
return;
if (disposing) {
// 釋放 managed resource
if (resource!= null) resource.Dispose();
}
// 釋放 unmanaged resource
_disposed = true;
}
}
你在寫一個新的 class 時首先要考慮要不要在這個 class 實做 IDisposable
介面,以下有兩個建議可以參考:
- 當你寫的類型包含有實做
IDisposable
的成員時,請實做 Dispose Pattern。 - 當你寫的類型沒有包含實做
IDisposable
的成員和 unmanaged 資源,但衍生的類別會有包含,考慮實做 Dispose Pattern。
第一點在做法 15 有提到,這種寫法主要在避免重複創建過多的物件。
public class MyResource : IDisposable
{
FileStream fileStream = new FileStream(@"c:\test1.txt", FileMode.Open);
public void Dispose()
{
fileStream.Dispose();
}
}
第二點有一個例子是 Stream
抽象類別就有實做 IDisposable
介面,因為它的衍生類別例如 FileStream
都需要 Dispose
資源因此直接在
底層的抽象類別實做 IDisposable
介面。
接下來看一下 Dispose Pattern
裡面為什麼需要這麼多內容,第一個問題是為什麼需要在寫一個 Dispose
多載方法
protected virtual void Dispose(bool disposing) {
if (disposing) {
// 釋放 managed resource
if (resource!= null) resource.Dispose();
}
// 釋放 unmanaged resource
}
原因是 finalizer
與 Dispose
方法基本上要做的事情是相同的,一個是加入到 Finalize queue 等待清除一個是呼叫後馬上清除,
所以它們之間會有許多重複的邏輯,所以建議是把這些清除邏輯都搬到 Dispose
多載方法,這樣 finalizer
與 Dispose
只需要呼叫多載方法就好。
下一個是 Dispose 方法為什麼要寫成這樣?
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
~DisposableResourceHolder() {
Dispose(false);
}
呼叫 Dispose
方法代表我們需要即時釋放掉記憶體資源,所以可以確認 Dispose
方法前物件還存活著,finalizer
則是 GC 確認這個物件已經
不需要時會把它排入到 Finalize queue 等待,一段時間後才會呼叫你寫的 finalizer
方法,注意到這裡提到的一段時間沒有人能確定是多久,因此
很有可能你想釋放的物件已經釋放掉了。
所以 Dispose
方法裡面通常都是寫 Dispose(true)
並馬上接著 GC.SuppressFinalize(this)
,因為馬上呼叫 Dispose
方法可以確定
managed resource
與 unmanaged resource
都會執行到釋放邏輯,因此就可以搭配 SuppressFinalize
通知 GC 不要把這個物件加入到
Finalize queue 裡面了。
但是如果忘記呼叫 Dispose
方法,也只需要寫 Dispose(false)
就好,因為 managed resource
GC 自己會想辦法清除掉也有可能執行 finalizer
前已經清除掉了,
所以 finalizer
就不需要主動釋放 managed resource
了。
另外還有幾個補充建議可以讓 Dispose Pattern
更加實用
- 不要把無參數的
Dispose
方法宣告成virtual
。 - 讓 Dispose(bool) 能夠被重複呼叫而不會導致問題。
第一點很容易懂,如果宣告成 virtual
那衍生的類別就會 override
基底類別的 Dispose
方法,可能會導致基底類別無法釋放。
第二點只需要加上一個 flag
就可以達成想要的效果,以下寫法新增了一個 _disposed
欄位,這樣就能確保釋放邏輯只會跑一次。
public class DisposableResourceHolder :IDisposable
{
bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
// cleanup
_disposed = true;
}
}
Summary
這個做法學到了 Dispose Pattern
的標準做法還有它為什麼是這樣實踐的,最完整的做法可以參考 SafeHandle
這個類別,基本上就是把
上面提到的建議都寫在 SafeHandle
裡面了,所以實務上其實也可以從 SafeHandle
衍生出自己的類別這樣可以省事不少。