C# Dispose模式

说不出来發表於2024-03-14

C# 中的 Dispose 模式是實現 IDisposable 的介面並釋放類中持有的非託管資源的機制,這個模式的目的的是為了及時釋放寶貴的非託管資源和託管資源,並且保證資源在被gc回收的時候可以正確釋放資源,同時兼顧執行效率。

編寫Dispose模式時應考慮以下問題

  1. 什麼時候需要呼叫finalizer?
  2. 什麼情況下需要虛擬Dispose方法?
  3. 如果已經呼叫過了Dispose方法怎麼處理?
  4. 呼叫派生類上的Dispose方法怎麼確保基類的資源被正確釋放?

例子一,只包含託管資源

public sealed class MyDisposableClass : IDisposable
{
    private readonly SqlConnection _sqlConnection;
    private bool _alreadyDisposed;

    public MyDisposableClass(string connectionString)
    {
        _sqlConnection = new SqlConnection(connectionString);
    }
    
    public void MyClassPublicMethod()
    {
        if (_alreadyDisposed)
        {
            throw new ObjectDisposedException(nameof(MyDisposableClass));
        }
        
        // Method implementation
    }
    
    public void Dispose()
    {
        if(_alreadyDisposed)
            return;
        
        _sqlConnection.Dispose();
        _alreadyDisposed = true;
    }
}

以上方法包含了一個_alreadyDisposed欄位來指示如果_sqlConnection資料庫連線已經釋放過一次則不再進行釋放,確保不會多次呼叫到產生報錯,但是這個示例中只涉及了託管資源的釋放所以這裡沒有編寫終結器相關的程式碼(Finalizers)

當前僅當程式碼中涉及到了非託管資源的存在時才需要終結器(Finalizers)的存在

例子二,包含託管資源和非託管資源

public sealed class MyDisposableClass : IDisposable
{
    private readonly SqlConnection _sqlConnection;
    private readonly IntPtr _unmanagedPointer;
    private bool _alreadyDisposed;

    public MyDisposableClass(string connectionString)
    {
        _sqlConnection = new SqlConnection(connectionString);
        _unmanagedPointer = Marshal.AllocHGlobal(100 * 1024 * 1024);
    }
    
    public void MyClassPublicMethod()
    {
        if (_alreadyDisposed)
        {
            throw new ObjectDisposedException(nameof(MyDisposableClass));
        }
        
        // Method implementation
    }
    
    public void Dispose()
    {
        if(_alreadyDisposed)
            return;
        
        _sqlConnection.Dispose();
        _alreadyDisposed = true;
        
        Marshal.FreeHGlobal(_unmanagedPointer);

        GC.SuppressFinalize(this);
    }

    ~MyDisposableClass()
    {
        if(_alreadyDisposed)
            return;
            
        Marshal.FreeHGlobal(_unmanagedPointer);
    }
}

這個例子程式碼中包含了一個指向 100 MB 非託管記憶體塊的指標,這個指標假設是和外界進行程式碼互動用的,不屬於託管資源,不會被.NET GC正確釋放,在這裡我們可以看到在兩個地方清理了非託管記憶體 - 在 Dispose() 方法和 ~MyDisposableClass() 終結器中。如果呼叫者正常呼叫了 Dispose() 方法,所有資源將立即被清理。此時,已經完成了處理,因此我們透過呼叫 GC.SuppressFinalize(this) 指示 GC 不要執行終結器。但是為了預防因為某些原因忘記呼叫 Dispose(),資源清理將在將來某個時候 GC 呼叫我們的終結器時去釋放對應的資源。

為什麼我們終結器中處理 SqlConnection 資源呢?垃圾物件的終結器可以以完全不確定的方式以任何順序執行。根本無法保證兩個超出範圍的物件之間哪個終結器將首先執行。這意味著當我們的終結器被呼叫時,SqlConnection 可能已經被終結。這就是為什麼在終結器中,我們只能處理 GC 不知道如何處理的非託管資源。此時,託管物件不在我們的控制範圍內。

將以上程式碼做一定的整合,則

例子三

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

private void Dispose(bool disposing)
{
    if(_alreadyDisposed)
        return;

    if (disposing)
    {
        _sqlConnection.Dispose();
        _alreadyDisposed = true;
    }
    
    Marshal.FreeHGlobal(_unmanagedPointer);
}

~MyDisposableClass()
{
    Dispose(false);
}

透過兩個bool標誌位來確保多次呼叫的情況下能正常工作。

如果考慮繼承並正常處理基類的情況的話,則可以將可以將Dispose設定成虛方法讓整個繼承鏈下的類自行釋放自身的資源

例子四

public class MyDerivedDisposableClass : MyDisposableClass
{
    private readonly FileStream _fileStream;
    private readonly IntPtr _unmanagedPointer;
    private bool _alreadyDisposed;


    public MyDerivedDisposableClass(string path, string connectionString) : base(connectionString)
    {
        _fileStream = new FileStream(path, FileMode.Open);
        _unmanagedPointer = Marshal.AllocHGlobal(100 * 1024 * 1024);
    }
    
    public void MyDerivedClassPublicMethod()
    {
        if (_alreadyDisposed)
        {
            throw new ObjectDisposedException(nameof(MyDerivedDisposableClass));
        }
        
        // 做一些其他事情
    }

    protected override void Dispose(bool disposing)
    {
        if(_alreadyDisposed)
            return;
        
        if (disposing)
        {
            _fileStream.Dispose();
        }
        
        Marshal.FreeHGlobal(_unmanagedPointer);
        _alreadyDisposed = true;
        
        base.Dispose(disposing); // 呼叫父類的處理方法,釋放父類的資源
    }

    ~MyDerivedDisposableClass()
    {
        Dispose(false);
    }
}

完整的呼叫流程如下

  1. 呼叫公共 Dispose() 方法,例如IDisposable 實現。該方法是派生類公共介面的一部分,因為它是從基類繼承的。
  2. Dispose() 呼叫受保護的虛擬 Dispose(disposing) 方法,並設定 disposing=true。此時,由於多型性,該方法的派生類實現被呼叫。
  3. 派生類 Dispose(disposing)負責釋放所有託管和非託管資源。
  4. 派生類呼叫父類實現Dispose(disposing)。
  5. 基類清理其託管和非託管資源。
  6. 派生類和基類 Dispose(dispose) 執行都已完成,程式流程返回到公共 Dispose() 方法。
  7. Dispose() 呼叫 GC.SupressFinalize(this) 並執行完成。

https://vkontech.com/the-dispose-pattern-step-by-step/

相關文章