這個做法討論了泛型中一個特殊情況,就是當傳入的類型參數(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 介面的參數時就必須產生對應的處理方式,這個並沒有最佳 解法要看實際情況來決定,所以關鍵是要有防衛性的思考模式才不會導致記憶體洩漏問題。