Effective C# 17.實作標準的 Dispose 模式 Effective C# 17.實作標準的 Dispose 模式 (Implement the Standard Dispose Pattern)

Published on Monday, October 14, 2024

在前幾個作法都有提到 IDisposable 的相關概念,這個作法就是在討論如何寫出標準的 Dispose 模式。

之前也有提到實做 IDisposable 介面時使用者需要自行注意釋放的時機,因此最方便的做法就是搭配 using 這樣內部程式碼結束後會自動呼叫 Dispose 方法,為了避免使用者忘記釋放掉記憶體或不知道要使用 using 來釋放,我們在實做的時候可以選擇將 finalizerIDisposable 結合起來的寫法 ,這樣即可以手動釋放也可以避免使用者忘記的情況發生。

以下是實做 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 介面,以下有兩個建議可以參考:

  1. 當你寫的類型包含有實做 IDisposable 的成員時,請實做 Dispose Pattern。
  2. 當你寫的類型沒有包含實做 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
}

原因是 finalizerDispose 方法基本上要做的事情是相同的,一個是加入到 Finalize queue 等待清除一個是呼叫後馬上清除, 所以它們之間會有許多重複的邏輯,所以建議是把這些清除邏輯都搬到 Dispose 多載方法,這樣 finalizerDispose 只需要呼叫多載方法就好。

下一個是 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 resourceunmanaged resource 都會執行到釋放邏輯,因此就可以搭配 SuppressFinalize 通知 GC 不要把這個物件加入到 Finalize queue 裡面了。

但是如果忘記呼叫 Dispose 方法,也只需要寫 Dispose(false) 就好,因為 managed resource GC 自己會想辦法清除掉也有可能執行 finalizer 前已經清除掉了, 所以 finalizer 就不需要主動釋放 managed resource 了。

另外還有幾個補充建議可以讓 Dispose Pattern 更加實用

  1. 不要把無參數的 Dispose 方法宣告成 virtual
  2. 讓 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 衍生出自己的類別這樣可以省事不少。