.NET C#基礎(9):資源釋放

2023-09-11 18:00:46

1. 什麼是IDisposable?

  IDisposable介面是一個用於約定可進行釋放資源操作的介面,一個類實現該介面則意味著可以使用介面約定的方法Dispose來釋放資源。其定義如下:
public interface IDisposable
{
    void Dispose();
}
  上述描述中可能存在兩個問題:
  1. 什麼是「資源」?
  2. C#是一個執行在含有垃圾回收(GC)平臺上的語言,為什麼還需要手動釋放資源?

1.1 資源

  資源包括託管資源和非託管資源,具體來說,它可以是一個普通的類範例,是一個資料庫連線,是一個檔案指標,或者是一個視窗的控制程式碼等等。不太準確地說,你可以理解為就是程式執行時用到的各種東西。

1.2 為什麼要手動釋放資源

  對於託管資源,通常來說由於有GC的幫助,可以自動釋放回收而無需程式設計師手動管理。然而,由於C#允許使用非託管資源,這些非託管資源不受GC的控制,無法自動釋放回收,因此對於這類資源,就要程式設計師進行手動管理。
(ps:CLR,Common Language Runtime,即C#編譯後的IL程式碼的執行平臺)
  如果你寫過C++,這就相當於應該在範例銷燬時釋放掉成「new」出來的分配到堆上的資源,否則資源將一直保留在記憶體中無法釋放,導致記憶體漏失等一系列問題。
  在C++中,通常將資源釋放的操作放置在類的解構函式中,但C#並沒有解構函式這一概念,因此,C#使用IDisposable介面來對資源釋放做出約定——當程式設計師看到一個類實現IDisposable介面時,就應該想到在使用完該類的範例後就應該呼叫其Dispose方法來及時釋放資源。
  對於實現了IDispose介面的類,在C#中你通常可以採用如下方式來釋放資源:
  1:try...finally
UnmanagedResource resource = /* ... */;

try
{
    // 各種操作
}
finally
{
    resource.Dispose();
}

(注:在finally中釋放是為了確保即便執行時出錯也可以順利釋放資源)

  2:using

using (UnmanagedResource resource = /* ... */)
{
    // 離開using的作用域後會自動呼叫resource的Dispose方法
}

// 或者如果不需要額外控制作用域的簡寫
using UnmanagedResource resource = /* ... */;
(ps:實際上,哪怕不實現IDisposable介面,只要類實現了public void Dispose()方法都可以使用using進行管理)

 

2. 如何實現IDisposable

2.1 不太完美的基本實現

  你可能還會認為IDisposable很容易實現,畢竟它只有一個方法需要實現,並且看上去只要在方法裡釋放掉需要釋放的資源即可:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        // 釋放需要釋放的資源
    }    
}
  通常來說這樣做也不會有什麼大問題,然而,有幾個問題需要考慮。接下來將逐步闡述問題並給出解決方案。

2.2 如果使用者忘記了呼叫Dispose方法釋放資源

  儘管程式設計師都應該足夠細心來保證他們對那些實現了Disposable介面的類的範例呼叫Dispose方法,但是,出於各種原因,或許是他是一名新手,或許他受到老闆的催促,或許他昨天沒睡好等等,這些都可能導致他沒有仔細檢查自己的程式碼。
永遠不要假設你的程式碼會被一直正確地使用,總得留下些兜底的東西,提高健壯性——把你的使用者當做一個做著布朗運動的白痴,哪怕他可能是個經驗豐富的程式設計師,甚至你自己。
  對於這樣的問題,最自然的想法自然是交給GC來完成——如果程式設計師忘記了呼叫Dispose方法釋放資源,就留著讓GC來呼叫釋放。還好,C#允許你讓GC來幫助你呼叫一些方法——通過終端子。
  關於終端子的主題會是一個比較複雜的主題,因此在這裡不展開討論,將更多的細節留給其他主題。就本文而言,暫時只需要知道終端子的宣告方法以及GC會在「某一時刻」自動呼叫終端子即可。(你或許想問這個「某一時刻」是什麼時候,這實際上是需要交給複雜主題來討論的話題)
  宣告一個終端子類似於宣告一個構造方法,但是需要在方法的類名前新增一個~。如下:
class UnmanagedResource : IDisposable
{
    // UnmanagedResource的終端子
    ~UnmanagedResource()
    {
        // 一些操作
    }
}
    關於終端子,下面是一些你需要知道的:
    1:一個類中只能定義一個終端子,且終端子不能有任何存取修飾符(即不能新增public/private/protected/internal)
    2:永遠不要手動呼叫終端子(實際上你也無法這麼做)
  由於GC會在某一個時刻自動呼叫終端子,因此如果在終端子中呼叫Dispose方法,即使有粗心的程式設計師忘記了手動釋放資源,GC也會在某一時刻來幫他們兜底。如下:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        // 釋放需要釋放的資源
    }  

    ~UnmanagedResource()
    {
        // 終端子呼叫Dispose釋放資源
        Dispose();
    }
}

2.3 手動呼叫了Dispose後,終端子再次呼叫Dispose

  當你手動呼叫了Dispose方法後,並不表示你就告訴了GC不要再呼叫它的終端子,實際上,在你呼叫Dispose方法後,GC還是會在某一時刻呼叫終端子,而由於我們在終端子裡呼叫了Dispose方法,這會導致Dispose方法再次被呼叫——Double Free!
  當然,要解決這一問題非常簡單,只需要用一個欄位來表明資源是否被釋放,並在Dispose方法裡檢查這個欄位的值,一旦發現已經釋放則過就立刻返回。如下:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        // 如果已經釋放過就立刻返回
        if (_disposed)
        {
            return;
        }
   
        // 釋放需要釋放的資源
        
        // 標記已釋放
        _disposed = true;
    }  

    ~UnmanagedResource()
    {
        Dispose();
    }

    // 用於標記是否已經釋放的欄位
    private bool _disposed;
}
  這樣可以解決資源被重複釋放的問題,但是這還是無法阻止GC呼叫終端子。當然你或許會認為讓GC呼叫終端子沒什麼問題,畢竟我們保證了Dispose重複呼叫是安全的。不過,要知道終端子是會影響效能的,因此為了效能考慮,我們還是希望在Dispose方法呼叫後阻止終端子的執行(畢竟這時候已經不需要GC兜底了)。而要實現這一目標十分簡單,只需要在Dipose方法中使用GC.SuppressFinalize(this)告訴GC不要呼叫終端子即可。如下:
class UnmanagedResource : IDisposable
{
    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }
   
        // 釋放需要釋放的資源

        _disposed = true;
       
        // 告訴GC不要呼叫當前範例(this)的終端子
        GC.SuppressFinalize(this);
    }  

    ~UnmanagedResource()
    {
        Dispose();
    }

    private bool _disposed;
}
  這樣,如果呼叫了Dispose方法,就會「抑制」GC對終端子的呼叫;而讓終端子呼叫Dispose也不會產生什麼問題。

2.4 不是任何時候都需要釋放所有資源

  考慮一個比較複雜的類:
class UnmanagedResource : IDisposable
{
   // 其他程式碼
   
    private FileStream _fileStream;
}
  上述例子中,FileStream是一個實現了IDisposable的類,也就是說,FileStream也需要進行釋放。UnmanagedResource不僅要釋放自己的非託管資源,還要釋放FileStream。你或許認為只需要在UnmanagedResourceDispose方法中呼叫一下FileStreamDispose方法就行。如下:
class UnmanagedResource : IDisposable
{
    // 其它程式碼    
    
    public void Dispose()
    {
        // 其他程式碼

        _fileStream.Dispose();

        // 其它程式碼
    }

    private FileStream _fileStream;
}
  咋一看沒什麼問題,但是考慮一下,如果UnmanagedResourceDispose方法是由終端子呼叫的會發生什麼?
  提示:終端子的呼叫是無序的。
  是的,很可能FileStream的終端子先被呼叫了,執行過了其Dispose方法釋放資源,隨後UnmanagedResource的終端子呼叫Dispose方法時會再次呼叫FileStreamDispose方法——Double Free, Again。
  因此,如果Dispose方法是由終端子呼叫的,就不應該手動釋放那些本身就實現了終端子的託管資源——這些資源的終端子很可能先被執行。僅當手動呼叫Dispose方法時才手動釋放那些實現了終端子的託管資源。
  我們可以使用一個帶引數的Dispose方法,用一個引數來指示Dispose是否釋放託管資源。稍作調整,實現如下:
class UnmanagedResource : IDisposable
{
    // 其它程式碼
    private void Dispose(bool disposing)
    {
        // 其他程式碼
       
        if (disposing)
        {
            // 釋放託管資源
            _fileStream.Dispose();
        }
       
        // 釋放非託管資源

        // 其它程式碼
    }
}
  上述程式碼宣告了一個接受disposing引數的Dispose(bool disposing)方法,當disposingtrue時,同時釋放託管資源和非託管資源;當disposingfalse時,僅釋放託管資源。另外,為了不公開不必要的介面,將其宣告為private
  接下來,只需要在Dispose方法和終端子中按需呼叫Dispose(bool disposing)方法即可。
class UnmanagedResource : IDisposable
{
    // 其它程式碼

    public void Dispose()
    {
        // disposing=true,手動釋放託管資源
        Dispose(true);
        GC.SuppressFinalize(this);
    }    
    
    ~UnmanagedResource()
    {
        // disposing=false,不釋放託管資源,交給終端子釋放
        Dispose(false);
    }
    
    private void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }
   
        if (disposing)
        {
            // 釋放託管資源
        }

        // 釋放非託管資源

        _disposed = true;
    }
}

2.5 考慮一下子類的資源釋放

  考慮一下如果有UnmanagedResource的子類:
class HandleResource : UnmanagedResource
{
    private HandlePtr _handlePtr;
}
  HandleResource有自己的資源HandlePtr,顯然如果只是簡單繼承UnmanagedResource的話,UnmanagedResourceDispose方法並不能釋放HandleResourceHandlePtr
  那麼怎麼辦呢?使用多型,將UnmanagedResourceDispose方法宣告為virtual並在HandleResource裡覆寫;或者在HandleResource裡使用new重新實現Dispose似乎都可以:
// 使用多型
class UnmanagedResource : IDisposable
{
    public virtual void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
    public override void Dispose() { /* ... */}
}


// 重新實現
class UnmanagedResource : IDisposable
{
    public void Dispose() { /* ... */}
}
class HandleResource : UnmanagedResource
{
    public new void Dispose() { /* ... */}
}
  這兩種方法似乎都可行,但是一個很大的問題是,你還得對HandleResource重複做那些在它的父類別UnmanagedResource做過的事——解決重複釋放、定義終端子以及區分對待託管和非託管資源。

  這太不「繼承了」——顯然,有更好的實現方法。

  答案是:將UnmanagedResource的的Dispose(bool disposing)方法存取許可權更改為protected,並修飾為virtual,以讓子類存取/覆蓋:
class UnmanagedResource : IDisposable
{
    protected virtual void Dispose(bool disposing) { /* ... */ }
}
  這樣,子類可以通過覆寫Dispose(bool disposing)來實現自己想要的釋放功能:
class UnmanagedResource : IDisposable
{
    protected override void Dispose(bool disposing)
    {
        // 其他程式碼
        
        base.Dispose(disposing);
    }
}
(ps:建議先釋放子類資源,再釋放父類別資源)
  由於Dispose(bool disposing)是虛方法,因此父類別UnmanagedResource的終端子和Dispose方法中對Dispose(bool disposing)的呼叫會受多型的影響,呼叫到正確的釋放方法,故子類可以不必再做那些重複工作。

 

3. 總結

3.1 程式碼總覽

class UnmanagedResource : IDisposable
{
    // 對IDisposable介面的實現
    public void Dispose()
    {
        // 呼叫Dispose(true),同時釋放託管資源與非託管資源
        Dispose(true);
        // 讓GC不要呼叫終端子
        GC.SuppressFinalize(this);
    }    
    
    // UnmanagedResource的終端子
    ~UnmanagedResource()
    {
        // 呼叫Dispose(false),僅釋放非託管資源,託管資源交給GC處理
        Dispose(false);
    }
    
    // 釋放非託管資源,並可以選擇性釋放託管資源,且可以讓子類覆寫的Dispose(bool disposing)方法
    protected virtual void Dispose(bool disposing)
    {
        // 防止重複釋放
        if (_disposed)
        {
            return;
        }
       
        // disposing指示是否是否託管資源
        if (disposing)
        {
            // 釋放託管資源
        }

        // 釋放非託管資源
        
        // 標記已釋放
        _disposed = true;
    }
}

 

參考資料/更多資料:

【1】:IDisposable 介面

【2】:實現 Dispose 方法