1. 程式人生 > >為什麼要實現 IDisposable 介面?

為什麼要實現 IDisposable 介面?

一、背景

最近在精讀 《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() 方法內部重複的程式碼提煉出來