Dispose 模式

注意

此內容是由 Pearson Education, Inc. 授權轉載自架構設計指導方針:可重複使用 .NET 程式庫的慣例、慣用語和模式,第 2 版。 該版於 2008 年出版,該書自那以後已於第三版進行了全面修訂。 此頁面上的某些資訊可能已過期。

所有程式都會在執行期間取得一或多個系統資源,例如記憶體、系統控制代碼或資料庫連線。 開發人員在使用這類系統資源時必須謹慎,因為在取得和使用資源之後必須加以釋出。

CLR 支援自動記憶體管理。 受控記憶體 (使用 C# 運算子 new 配置的記憶體) 不需要明確釋放。 記憶體回收行程 (GC) 會自動釋放。 這可讓開發人員不必再煩惱釋放記憶體的繁瑣且困難工作,且是 .NET Framework 提供空前生產力的主要原因之一。

可惜的是,受控記憶體只是許多系統資源類型的其中一種。 受控記憶體以外的資源仍需要明確釋放,且稱為非受控資源。 GC 特別不是針對管理這類非受控資源所設計,這表示開發人員必須負責管理非受控資源。

CLR 提供一些釋放非受控資源的說明。 System.Object 宣告虛擬方法 Finalize (也稱為完成項),該完成項是在 GC 回收物件記憶體之前由 GC 所呼叫,並可覆寫以釋放非受控資源。 覆寫完成項的類型稱為可完成的類型。

雖然完成項在某些清除案例中有效,但仍有兩個顯著的缺點:

  • 當 GC 偵測到物件符合集合資格時,就會呼叫完成項。 在不再需要資源之後的一段未定時間,就會發生這個情況。 在需要許多稀少資源 (容易耗盡的資源) 的程式中,或在資源耗用成本很高 (大量非受控記憶體緩衝區) 的情況下,可能無法接受在開發人員可能或想要釋出資源時、與完成項實際釋放資源時間之間發生的延遲。

  • 當 CLR 需要呼叫完成項時,其必須延後收集物件的記憶體,直到下一輪記憶體回收 (完成項在集合之間執行) 為止。 這表示物件的記憶體 (及其所參考的所有物件) 將不會釋放較長的時間。

因此,在許多案例中,例如需要儘快回收非受控資源、處理稀少的資源時,或在無法接受新增完成項 GC 額外負荷的高效能案例中,單單仰賴完成項可能並不適當。

Framework 提供 System.IDisposable 介面,應實作此介面,讓開發人員在不需要非受控資源時,能立即以手動方式釋放這些非受控資源。 其也會提供 GC.SuppressFinalize 方法,向 GC 告知物件已手動處置,且不再需要完成,在此情況下,可以提前回收物件的記憶體。 實作 IDisposable 介面的類型稱為可處置的類型。

處置模式旨在標準化完成項和 IDisposable 介面的使用方式和實作。

模式的主要動機是減少實作 FinalizeDispose 方法的複雜度。 複雜度源自於方法共用部分但並非全部程式碼路徑 (本章節稍後會說明差異)。 此外,針對與具決定性資源管理的語言支援演進相關的模式元素,有一些歷程原因。

✓ 務必在包含可處置類型執行個體的類型上實作基本處置模式。 如需基本模式的詳細資料,請參閱基本處置模式一節。

如果類型負責其他可處置物件的存留期,則開發人員也需要一種方式來處置這些物件。 使用容器的 Dispose 方法是一種達成此動作的便利方式。

✓ 務必實作基本處置模式,並在保存需要明確釋放且沒有完成項的資源型別上提供完成項。

例如,應該在儲存非受控記憶體緩衝區的類型上實作模式。 可完成的類型一節討論與實作完成項相關的指導方針。

✓ 考慮在本身不會保存非受控資源或可處置物件的類別上實作基本處置模式,但可能有會保存的子類型。

這是 System.IO.Stream 類別的絕佳範例。 雖然其為不會保存任何資源的抽象基底類別,但大部分的子類別都會保留,因此會實作此模式。

基本處置模式

模式的基本實作牽涉到實作 System.IDisposable 介面,並宣告 Dispose(bool) 方法,以實作 Dispose 方法與選擇性完成項之間要共用的所有資源清除邏輯。

下列範例會示範基本模式的簡單實作:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

布林參數 disposing 會指出方法是從 IDisposable.Dispose 實作還是從完成項叫用。 Dispose(bool) 實作應該先檢查參數,然後再存取其他參考物件 (例如上述範例中的資源欄位)。 僅在從 IDisposable.Dispose 實作呼叫方法 (當 disposing 參數等於 true) 時,才應該存取這類物件。 如果從完成項叫用方法 (disposing 為 false),則不應該存取其他物件。 原因是物件會以無法預期的順序完成,因此物件或其任何相依性可能已經完成。

此外,本節也適用於基底尚未實作處置模式的類別。 如果您要繼承自已實作模式的類別,只需覆寫 Dispose(bool) 方法,即可提供額外的資源清除邏輯。

✓ 務必宣告 protected virtual void Dispose(bool disposing) 方法,以集中處理與釋放非受控資源相關的所有邏輯。

所有資源清除都應該發生在此方法中。 會從完成項和 IDisposable.Dispose 方法呼叫方法。 如果是從完成項內部叫用參數,則參數會是 false。 其應該用來確保完成期間執行的任何程式碼,全都不會存取其他可完成的物件。 下一節將說明實作完成項的詳細資料。

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

✓ 務必實作 IDisposable 介面,方法是只要呼叫 Dispose(true) 後面接著 GC.SuppressFinalize(this)

僅在 Dispose(true) 成功執行時,才應該呼叫 SuppressFinalize

public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
}

X 請勿讓無參數 Dispose 方法變成虛擬方法。

Dispose(bool) 方法是子類別應該覆寫的方法。

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X 請勿宣告 Dispose()Dispose(bool) 以外 Dispose 方法的任何多載。

Dispose 應視為保留字,以協助撰寫此模式的程式碼,並防止實作者、使用者和編譯器造成混淆。 某些語言可能會選擇在特定類型上自動實作此模式。

✓ 務必允許多次呼叫 Dispose(bool) 方法。 方法可能會在第一次呼叫之後,選擇不執行任何動作。

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X 避免Dispose(bool) 內擲回例外狀況,除非在所包含的程序已損毀 (外洩、不一致的共用狀態等) 嚴重情況下擲回例外狀況。

使用者預期呼叫 Dispose 不會引發例外狀況。

如果 Dispose 可能會引發例外狀況,則不會執行進一步最後封鎖清除邏輯。 若要解決此問題,使用者必須在 try 區塊中包裝對 Dispose 的每個呼叫 (在 finally 區塊中!),這會導致非常複雜的清除處理常式。 如果執行 Dispose(bool disposing) 方法,則在處置為 false 時,請勿擲回例外狀況。 這麼做會在完成項內容內執行時終止進程。

✓ 務必從處置物件之後無法使用的任何成員擲回 ObjectDisposedException

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

✓ 考慮提供 Close() 方法 (除了 Dispose() 之外),如果 close 是區域中的標準術語。

如果這麼做,請務必讓 Close 實作與 Dispose 相同,並考慮明確地實作 IDisposable.Dispose 方法。

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

可完成的類型

可完成的類型是藉由覆寫完成項並在 Dispose(bool) 方法中提供完成程式碼路徑,來擴充基本處置模式的類型。

完成項非常難以正確實作,主要是因為您無法對系統執行期間的系統狀態做出 (通常為有效) 假設。 應仔細考量下列指導方針。

請注意,某些指導方針不僅適用於 Finalize 方法,也適用於從完成項呼叫的任何程式碼。 在先前定義的基本處置模式案例中,這表示當 disposing 參數為 false 時,在 Dispose(bool disposing) 內部執行的邏輯。

如果基底類別已經可完成並實作基本處置模式,您就不應該再次覆寫 Finalize。 您應該改為僅覆寫 Dispose(bool) 方法來提供額外的資源清除邏輯。

下列程式碼顯示可完成類型的範例:

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

X 避免讓類型變成可完成的類型。

請仔細考慮您認為需要完成項的任何情況。 從效能和程式碼複雜度的觀點來看,會有與包含完成項的執行個體相關聯的實際成本。 偏好使用資源包裝函式 (例如 SafeHandle) 盡可能封裝非受控資源,在此情況下,完成項會變得不必要,因為包裝函式要負責自己的資源清除。

X 請勿讓實值型別變成可完成的實值型別。

只有參考型別實際上是由 CLR 完成,因此會忽略對實值型別放置完成項的任何嘗試。 C# 和 C++ 編譯器會強制執行此規則。

✓ 務必讓類型成為可完成的類型,前提是類型負責釋放沒有自有完成項的非受控資源。

實作完成項時,只要呼叫 Dispose(false) 並將所有資源清除邏輯放在 Dispose(bool disposing) 方法內即可。

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

✓ 務必在每個可完成的類型上實作基本處置模式。

這可為此類型的使用者提供一個方法,明確執行完成項所負責相同資源的決定性清除。

X 請勿存取完成項程式碼路徑中的任何可完成物件,因為會對其已經完成的物件造成重大風險。

例如,具有另一個可完成物件 B 參考的可完成物件 A 無法可靠地在 A 的完成項中使用 B,反之亦然。 會以隨機順序呼叫完成項 (缺乏對於重大完成的弱式排序保證)。

此外,請注意,會在應用程式定義域卸載或在結束程序時,於特定點收集儲存在靜態變數中的物件。 如果 Environment.HasShutdownStarted 傳回 true,則存取參考可完成物件的靜態變數 (或呼叫靜態方法,其可能會使用儲存在靜態變數中的值) 可能並不安全。

✓ 務必讓您的 Finalize 方法受到保護。

C#、C++ 和 VB.NET 開發人員不需要擔心這個問題,因為編譯器有助於強制執行此指導方針。

X 請勿允許例外狀況從完成項邏輯逸出,但系統關鍵性失敗除外。

如果從完成項擲回例外狀況,則 CLR 會關閉整個程序 (從 .NET Framework 2.0 版起),以防止其他完成項以受控方式執行及釋放資源。

✓ 考慮建立及使用重要的可完成物件 (類型階層中包含 CriticalFinalizerObject 的類型),即使遇到強制應用程式定義域卸載和執行緒中止,完成項還是必須執行。

Portions © 2005, 2009 Microsoft Corporation. 著作權所有,並保留一切權利。

獲 Pearson Education, Inc. 的授權再版,從 Krzysztof Cwalina 和 Brad Abrams 撰寫,並在 2008 年 10 月 22 日由 Addison-Wesley Professional 出版,作為 Microsoft Windows Development Series 一部份的 Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition 節錄。

另請參閱