實作 DisposeAsync 方法

System.IAsyncDisposable 介面已導入做為 C# 8.0 的一部分。 當需要執行資源清除時,您可實作 IAsyncDisposable.DisposeAsync() 方法,就像實作 Dispose 方法一樣。 不過,其中一個主要差異是,此實作允許非同步清除作業。 DisposeAsync() 會傳回代表非同步處置作業的 ValueTask

通常,在實作 IAsyncDisposable 介面時,類別也會實作 IDisposable 介面。 IAsyncDisposable 介面的良好實作模式是針對同步或非同步處置做好準備,不過這不是必要條件。 如果無法同步處置類別,則只能接受 IAsyncDisposable。 實作處置模式的所有指導方針也適用於非同步實作。 本文假設您已熟悉如何實作 Dispose 方法

警告

如果您實作 IAsyncDisposable 介面,而不是 IDisposable 介面,您的應用程式可能會流失資源。 如果類別實作 IAsyncDisposable,而不是 IDisposable,且取用者只會呼叫 Dispose,則實作永遠不會呼叫 DisposeAsync。 這會導致資源流失。

提示

就相依性插入而言,在 IServiceCollection 註冊服務時,系統會代表您隱含管理服務存留期IServiceProvider 和對應的 IHost 會協調資源清除。 具體而言,IDisposableIAsyncDisposable 的實作會在指定的存留期結束時正確處置。

如需詳細資訊,請參閱 .NET 的相依性插入

探索 DisposeAsyncDisposeAsyncCore 方法

IAsyncDisposable 介面會宣告單一無參數方法,DisposeAsync()。 任何非密封類別都應該定義也傳回 ValueTaskDisposeAsyncCore() 方法。

  • 沒有參數的 publicIAsyncDisposable.DisposeAsync() 實作。

  • 特徵標記為下列 protected virtual ValueTask DisposeAsyncCore() 方法:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

DisposeAsync 方法

public 無參數 DisposeAsync() 方法會在 await using 陳述式隱含呼叫,其目的是釋放非受控資源、執行一般清除,以及指出完成項 (如果存在),則不需要執行。 釋放與 Managed 物件相關聯的記憶體一律是記憶體回收行程的網域。 因此,它擁有標準實作:

public async ValueTask DisposeAsync()
{
    // Perform async cleanup.
    await DisposeAsyncCore();

    // Dispose of unmanaged resources.
    Dispose(false);

    // Suppress finalization.
    GC.SuppressFinalize(this);
}

注意

與處置模式相比,非同步處置模式的主要差異之一,就是從 DisposeAsync()Dispose(bool) 多載方法的呼叫會以引數的形式提供 false 。 不過,實作 IDisposable.Dispose() 方法時,會改為傳遞 true。 這有助於確保與同步處置模式的功能等價,並進一步確保仍然會叫用完成項程式碼路徑。 換句話說,DisposeAsyncCore() 方法會以非同步方式處置受控資源,因此您也不想以同步方式處置它們。 因此,呼叫 Dispose(false) 而不是呼叫 Dispose(true)

DisposeAsyncCore 方法

DisposeAsyncCore() 方法旨在執行受控資源的非同步清除,或對 DisposeAsync() 進行串聯呼叫。 當子類別繼承作為 IAsyncDisposable 實作的基底類別時,它會封裝常見的非同步清除作業。 DisposeAsyncCore() 方法是 virtual,因此衍生類別可在其覆寫定義自訂清除。

提示

如果 IAsyncDisposable 的實作是 sealed,則不需要 DisposeAsyncCore() 方法,而且非同步清除可以直接在 IAsyncDisposable.DisposeAsync() 方法執行。

實作非同步處置模式

所有非密封類別都應該視為潛在的基底類別,因為它們可以繼承。 如果您實作任何潛在基底類別的非同步處置模式,則必須提供 protected virtual ValueTask DisposeAsyncCore() 方法。 下列部分範例使用 NoopAsyncDisposable 類別,其定義如下:

public sealed class NoopAsyncDisposable : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}

以下是使用 NoopAsyncDisposable 型別之非同步處置模式的範例實作。 會傳回 ValueTask.CompletedTask 來型別實作 DisposeAsync

public class ExampleAsyncDisposable : IAsyncDisposable
{
    private IAsyncDisposable? _example;

    public ExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_example is not null)
        {
            await _example.DisposeAsync().ConfigureAwait(false);
        }

        _example = null;
    }
}

在前述範例中:

  • ExampleAsyncDisposable 是實作 IAsyncDisposable 介面的非密封類別。
  • 其中包含在建構函式初始化的私人 IAsyncDisposable 欄位 _example
  • DisposeAsync 方法會委派給 DisposeAsyncCore 方法,並呼叫 GC.SuppressFinalize,以通知記憶體回收行程完成項不需要執行。
  • 它包含呼叫 _example.DisposeAsync() 方法的 DisposeAsyncCore() 方法,並將欄位設定為 null
  • DisposeAsyncCore() 方法是 virtual,允許子類別以自訂行為覆寫它。

密封的替代非同步處置模式

如果您的實作類別可以是 sealed,您可以覆寫 IAsyncDisposable.DisposeAsync() 方法來實作非同步處置模式。 下列範例示範如何實作密封類別的非同步處置模式:

public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
    private readonly IAsyncDisposable _example;

    public SealedExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public ValueTask DisposeAsync() => _example.DisposeAsync();
}

在前述範例中:

  • SealedExampleAsyncDisposable 是實作 IAsyncDisposable 介面的密封類別。
  • 包含 _example 的欄位為 readonly,並在建構函式初始化。
  • DisposeAsync 方法會呼叫 _example.DisposeAsync() 方法,透過包含欄位實作模式 (串聯處理)。

實作處置與非同步處置模式

您可能需要同時實作 IDisposableIAsyncDisposable 介面,特別是當您的類別範圍包含這些實作的執行個體時。 這麼做可確保您可以適當地串聯清除呼叫。 以下是實作這兩個介面的範例類別,並示範清除的適當指導。

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    IDisposable? _disposableResource = new MemoryStream();
    IAsyncDisposable? _asyncDisposableResource = new MemoryStream();

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

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            _disposableResource = null;

            if (_asyncDisposableResource is IDisposable disposable)
            {
                disposable.Dispose();
                _asyncDisposableResource = null;
            }
        }
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_asyncDisposableResource is not null)
        {
            await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }

        if (_disposableResource is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _disposableResource?.Dispose();
        }

        _asyncDisposableResource = null;
        _disposableResource = null;
    }
}

IDisposable.Dispose()IAsyncDisposable.DisposeAsync() 實作都是簡單的重複使用程式碼。

Dispose(bool) 多載方法中,如果執行個體不是 null,則會有條件的處置 IDisposable 執行個體。 IAsyncDisposable 執行個體會被強制型轉為 IDisposable,而且如果它也不是 null,也會被處置。 然後,這兩個執行個體都會指派給 null

使用 DisposeAsyncCore() 方法時,會遵循相同的邏輯方法。 如果 IAsyncDisposable 執行個體不是 null,則會等候對 DisposeAsync().ConfigureAwait(false) 的呼叫。 如果 IDisposable 執行個體也是 IAsyncDisposable 的實作,它也會以非同步方式處置。 然後,這兩個執行個體都會指派給 null

每個實作都會努力處置所有可能的可處置物件。 這可確保清除已正確重疊。

使用非同步可處置項目

若要正確取用實作 IAsyncDisposable 介面的物件,請同時使用 await 以及 using 關鍵字 。 請考慮下列範例,其中 ExampleAsyncDisposable 類別會具現化,然後包裝在 await using 陳述式。

class ExampleConfigureAwaitProgram
{
    static async Task Main()
    {
        var exampleAsyncDisposable = new ExampleAsyncDisposable();
        await using (exampleAsyncDisposable.ConfigureAwait(false))
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

重要

使用 IAsyncDisposable 介面的 ConfigureAwait(IAsyncDisposable, Boolean) 擴充方法,設定如何在其原始內容或排程器封送處理工作接續。 深入了解 ConfigureAwait,請參閱 ConfigureAwait 常見問題集

針對不需要使用 ConfigureAwait 的情況,await using 陳述式可簡化如下:

class ExampleUsingStatementProgram
{
    static async Task Main()
    {
        await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

此外,可以編寫為使用 using 宣告的隱含範圍。

class ExampleUsingDeclarationProgram
{
    static async Task Main()
    {
        await using var exampleAsyncDisposable = new ExampleAsyncDisposable();

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

單行中的多個 await 關鍵字

有時候,await 關鍵字可能會在單行內出現多次。 例如,請考慮下列程式碼:

await using var transaction = await context.Database.BeginTransactionAsync(token);

在前述範例中:

堆疊使用

在建立及使用實作 IAsyncDisposable 的多個物件的情況,將 await using 陳述式與 ConfigureAwait 堆疊可能會阻止在錯誤情況下呼叫 DisposeAsync()。 若要確保一律呼叫 DisposeAsync(),應該避免堆疊。 下列三個程式碼範例示範改用可接受的模式。

可接受的模式一


class ExampleOneProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.

            var objTwo = new ExampleAsyncDisposable();
            await using (objTwo.ConfigureAwait(false))
            {
                // Interact with the objOne and/or objTwo instance(s).
            }
        }

        Console.ReadLine();
    }
}

在上述範例中,每個非同步清除作業都會明確限定在 await using 區塊底下。 外部範圍會遵循 objOne 設定其大括弧將 objTwo 括起來的方式,例如先處置 objTwo,然後再處置 objOne。 這兩個 IAsyncDisposable 執行個體都會等候其 DisposeAsync() 方法,因此每個執行個體都會執行其非同步清除作業。 呼叫是巢狀而非堆疊。

可接受的模式二

class ExampleTwoProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.
        }

        var objTwo = new ExampleAsyncDisposable();
        await using (objTwo.ConfigureAwait(false))
        {
            // Interact with the objTwo instance.
        }

        Console.ReadLine();
    }
}

在上述範例中,每個非同步清除作業都會明確限定在 await using 區塊底下。 在每個區塊結束時,對應的 IAsyncDisposable 執行個體會等候其 DisposeAsync() 方法,進而執行其非同步清除作業。 呼叫是循序而非堆疊。 在此案例,會先處置 objOne,然後處置 objTwo

可接受的模式三

class ExampleThreeProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using var ignored1 = objOne.ConfigureAwait(false);

        var objTwo = new ExampleAsyncDisposable();
        await using var ignored2 = objTwo.ConfigureAwait(false);

        // Interact with objOne and/or objTwo instance(s).

        Console.ReadLine();
    }
}

在上述範例,每個非同步清除作業都會以包含的方法主體隱含限定範圍。 在封閉區塊結束時,IAsyncDisposable 執行個體會執行其非同步清除作業。 這個範例會以宣告它們的反向循序執行,這表示會在 objOne 之前處置 objTwo

無法接受的模式

下列程式碼反白顯示的幾行會顯示「堆疊使用」的意義。 如果從 AnotherAsyncDisposable 建構函式擲回例外狀況,則不會正確處置這兩個物件。 因為建構函式未順利完成,所以永遠不會指派變數 objTwo。 因此,AnotherAsyncDisposable 的建構函式會負責處置在擲回例外狀況之前所配置的任何資源。 ExampleAsyncDisposable 如果類型具有完成項,則符合完成資格。

class DoNotDoThisProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        // Exception thrown on .ctor
        var objTwo = new AnotherAsyncDisposable();

        await using (objOne.ConfigureAwait(false))
        await using (objTwo.ConfigureAwait(false))
        {
            // Neither object has its DisposeAsync called.
        }

        Console.ReadLine();
    }
}

提示

請避免此模式,因為它可能會導致非預期的行為。 如果您使用其中一個可接受的模式,未公開的物件問題就不存在。 未堆疊 using 陳述式時,會正確執行清除作業。

另請參閱

如需 IDisposableIAsyncDisposable 的雙重實作範例,請參閱 GitHubUtf8JsonWriter原始程式碼。