Effective C# 21.建構支援 Disposable 型別參數的泛型類別 Effective C# 21.建構支援 Disposable 型別參數的泛型類別 (Always Create Generic Classes That Support Disposable Type Parameters)

Published on Friday, October 18, 2024

這個做法討論了泛型中一個特殊情況,就是當傳入的類型參數(type parameter)有實做 IDisposable 介面的時候,可能會導致記憶體釋放的問題。

例如下面這段程式碼,傳入的 Engine 類別有實做 IDisposable 介面,但是泛型類別的 driver 區域變數並沒有用 using 包裝起來, 所以並不會呼叫 Dispose 方法這樣會導致記憶體釋放的問題。

void Main()
{
	var xx = new EngineDriverOne<Engins>();
}

public class Engine : IEngine, IDisposable
{
	public void Dispose()
	{
	}

	public void DoWork()
	{
	}
}

public interface IEngine
{
	void DoWork();
}

public class EngineDriverOne<T> where T : IEngine, new()
{
	public void GetThingsDone()
	{
		T driver = new T();
		driver.DoWork();
	}
}

稍微修改一下上面的程式碼,這樣假如 T 沒有實作 IDisposable 介面那麼背後隱藏的區域變數會為 null,所以並不會呼叫 Dispose 方法。

public class EngineDriverOne<T> where T : IEngine, new()
{
	public void GetThingsDone()
	{
		T driver = new T();
		using (driver as IDisposable)
		{
			driver.DoWork();
		}
	}
}

編譯過後的程式碼大概會長這樣子。

IDisposable disposable = driver as IDisposable;
try
{
    driver.DoWork();
}
finally
{
    if (disposable != null)
    {
         disposable.Dispose();
    }
}

但是你如果把類型參數(type parameter)提升為成員變數,這樣就必須將目前的類別實作 IDisposable 介面,這在以前做法 17 有提到過。

public sealed class EngineDriver2<T> : IDisposable
   where T : IEngine, new()
{
	private Lazy<T> driver = new Lazy<T>(() => new T());
	public void GetThingsDone() => driver.Value.DoWork();
	public void Dispose()
	{
		if (driver.IsValueCreated)
		{
			var resource = driver.Value as IDisposable;
			resource?.Dispose();
		}
	}
}

但是在實務上上面的寫法不允許多次呼叫 Dispose 方法,這個也在做法 17 有提到,我們要能自己寫的 Dispose Pattern 能夠被重複執行, 因此建議乾脆把 Dispose 的責任移到別的類別上同時也可以把 new() 約束移除掉,避免建構 T 時可能要耗費的資源,也就不用考慮使用 Lazy 成員 導致的釋放問題了。

public sealed class EngineDriver<T> where T : IEngine
{
	// It's expensive to create, so initialize to null
	private T driver;
	public EngineDriver(T driver)
	{
		this.driver = driver;
	}
	public void GetThingsDone()
	{
		driver.DoWork();
	}
}

Summary

這個做法在建議實做泛型類別時要時刻考慮傳入的參數的類型,像是這種有實做 IDisposable 介面的參數時就必須產生對應的處理方式,這個並沒有最佳 解法要看實際情況來決定,所以關鍵是要有防衛性的思考模式才不會導致記憶體洩漏問題。