最近在精讀 《CLR Via C#》和 《Effective C#》 的時候,發現的一個問題點。通常來講,咱們實現 IDisposable
接口,是爲了釋放託管資源和非託管資源。不過在 C# 類型定義裏面有一個功能相似的東西,那就是 終結器。ide
最開始我是學 C++ 的,以後學 C# 的時候發現這玩意兒不管是寫法和做用,都跟 C++ 裏面的 析構函數 同樣。在 C++ 裏面的析構函數是在對象釋放的時候會被調用,以後這個觀點一直被我帶到 C#,認爲資源釋放的動做放在終結器不就好了麼。爲何還要我實現 IDisposable
接口,而後讓使用者手動釋放呢?函數
C++ 版本的析構函數:this
class Line { public: Line(); ~Line(); private: double length; };
C# 版本的終結器:線程
public class Line { private double _length; public Line() { } ~Line() { } }
提及這個緣由,首先得從 C# 終結器的 調用時機 提及。終結器的調用是 CLR 在進行 GC 時,若是某個對象寫有終結器,即使它應該被釋放,也不會立刻回收該對象。而 C++ 的析構函數是肯定性析構,取決於你調用 delete 的時機。code
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()
方法內部重複的代碼提煉出來。