一、背景
最近在精讀 《CLR Via C#》和 《Effective C#》 的時候,發現的一個問題點,一般來說,我們實作 IDisposable 介面,是為了釋放托管資源和非托管資源,不過在 C# 型別定義里面有一個功能類似的東西,那就是 終結器,
最開始我是學 C++ 的,之后學 C# 的時候發現這玩意兒不論是寫法和作用,都跟 C++ 里面的 解構式 一樣,在 C++ 里面的解構式是在物件釋放的時候會被呼叫,之后這個觀點一直被我帶到 C#,認為資源釋放的動作放在終結器不就行了么,為什么還要我實作 IDisposable 介面,然后讓使用者手動釋放呢?
C++ 版本的解構式:
class Line
{
public:
Line();
~Line();
private:
double length;
};
C# 版本的終結器:
public class Line
{
private double _length;
public Line()
{
}
~Line()
{
}
}
二、原因
說起這個原因,首先得從 C# 終結器的 呼叫時機 說起,終結器的呼叫是 CLR 在進行 GC 時,如果某個物件寫有終結器,即便它應該被釋放,也不會馬上回收該物件,而 C++ 的解構式是確定性析構,取決于你呼叫 delete 的時機,
GC 會將其添加到一個佇列當中,單獨使用了一個 高優先級 執行緒去呼叫物件的終結器,因為要保證執行緒能夠訪問到終結器物件,所以本該釋放的物件,以及物件相關的資源就 會被提升 1 代 ,會 增加記憶體占用,
一旦終結器方法帶有死回圈,那么 GC 將永遠無法釋放該資源,造成 記憶體泄漏,
除開記憶體占用增大的原因,如果你在終結器方法內部參考了其他帶終結器物件,GC 無法保證終結器呼叫順序,所以你可能訪問到的物件是已經終結了的,
還有一種情況會導致尷尬的記憶體泄漏,本來物件 A 應該被釋放了,結果你在終結器內部又讓其他的根保持物件的參考,又會讓這個物件復活,因為 GC 只會執行一次帶終結器物件的終結器,執行一次過后,就再也不會執行物件的終結器了,
public class BadClass
{
private static readonly List<BadClass> _list = new List<BadClass>();
private string _msg;
public BadClass(string msg)
{
_msg = (string)msg.Clone();
}
~BadClass()
{
// 造成 _msg 的記憶體不會被釋放,
_list.Add(this);
}
}
三、最佳實踐
針對 Effective C# 所提出的最佳實踐,你應該為物件實作 IDisposable 介面,以釋放托管資源,如果你物件確實使用了非托管資源,那么你也應該為其撰寫終結器,因為非托管資源的,你不能保證呼叫者能夠顯示呼叫 Dispose() 方法,所以你得通過終結器來處理,
一個典型的 Dispose() 方法應該將托管資源、非托管資源全部進行釋放,設定對應的標識表明物件已經被釋放了,阻止垃圾回收器重復清理該物件、保證方法的 冪等性,
public class FatherClass : IDisposable
{
private bool isDisposed = false;
public void Dispose()
{
Dispose(true);
// 通知 GC,這個物件已經完全被清理,
GC.SuppressFinalize(this);
}
~FatherClass()
{
Dispose(false);
}
protected virtual Dispose(bool isDisposing)
{
if(isDisposed) return;
if(isDisposing)
{
// 釋放托管資源,
}
// 釋放非托管資源,
isDisposed = true;
}
public void TestMethod()
{
if(isDisposed)
{
throw new ObjectDisposedException("物件已經被釋放,");
}
}
}
public class ChildClass : FatherClass
{
private bool isDisposed = false;
protected override void Dispose(bool isDisposing)
{
if(isDisposed) return;
if(isDisposing)
{
// 釋放托管資源,
}
base.Dispose(isDisposing);
isDisposed = true;
}
}
在上面的實踐中,我們提煉出了一個 void Dispose(bool) 方法,并將其設定為虛函式,這樣做的好處有兩點,第一點是方便子類重寫釋放邏輯,第二點是可以將終結器和 Dispose() 方法內部重復的代碼提煉出來,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/116642.html
標籤:C#
上一篇:C#用抽象類定義幾何圖形
下一篇:C#8.0 新增功能
