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