DisposeAsync メソッドの実装

System.IAsyncDisposable インターフェイスが、C# 8.0 の一部として導入されました。 IAsyncDisposable.DisposeAsync() メソッドは、Dispose メソッドの実装と同様、リソースのクリーンアップを実行する必要がある場合に実装します。 ただし、重要な違いの 1 つは、この実装により、非同期のクリーンアップ操作が可能になることです。 DisposeAsync() は、非同期の破棄操作を表す ValueTask を返します。

通常、IAsyncDisposable インターフェイスを実装するとき、そのクラスでは IDisposable インターフェイスも実装します。 IAsyncDisposable インターフェイスの推奨される実装パターンは、同期か非同期のいずれかの破棄のために準備をすることです。ただし、必須ではありません。 クラスの同期の破棄が不可能な場合は、IAsyncDisposable を持つことだけが許容されます。 破棄パターンの実装に関するガイダンスのすべての説明は、非同期の実装にも適用されます。 この記事では、読者が Dispose メソッドの実装方法について既に理解していることを前提としています。

注意事項

IAsyncDisposable インターフェイスを実装しても、IDisposable インターフェイスを実装していない場合、アプリによってリソースがリークされる可能性があります。 クラスによって IAsyncDisposable は実装されるが、IDisposable が実装されない場合、コンシューマーが Dispose のみを呼び出すと、実装で DisposeAsync が呼び出されることはありません。 これにより、リソース リークが発生します。

ヒント

依存関係の挿入に関して、サービスを IServiceCollection に登録すると、IServiceCollectionがお客様に代り暗黙的に管理されます。 IServiceProvider とそれに対応する IHost によって、リソースのクリーンアップが調整されます。 具体的には、IDisposable および IAsyncDisposable の実装は、それらに指定した有効期間の終了時に適切に破棄されます。

詳細については、「.NET での依存関係の挿入」を参照してください。

DisposeAsync メソッドと DisposeAsyncCore メソッドを調べる

IAsyncDisposable インターフェイスは、パラメーターなしの単一のメソッド DisposeAsync() を宣言します。 非シールド クラスでは、ValueTask も返す DisposeAsyncCore() メソッドを定義する必要があります。

  • パラメーターを持たない publicIAsyncDisposable.DisposeAsync() の実装。

  • 次のシグネチャを持つ protected virtual ValueTask DisposeAsyncCore() メソッド。

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

DisposeAsync メソッド

public のパラメーターなしの DisposeAsync() メソッドは、await using ステートメントで暗黙的に呼び出されます。その目的は、アンマネージド リソースを解放し、通常のクリーンアップを実行し、ファイナライザーが存在する場合、それを実行する必要がないことを示すことです。 管理されたオブジェクトに関連付けられているメモリを解放するのは、常にガベージ コレクターのドメインです。 このため、次のような標準的な実装があります。

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

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

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

注意

非同期の破棄パターンでの、その破棄パターンとの主な違いの 1 つは、DisposeAsync() から Dispose(bool) オーバーロード メソッドへの呼び出しで、false が引数として渡されることです。 一方、IDisposable.Dispose() メソッドを実装する場合は、代わりに true が渡されます。 これにより、同期の破棄パターンとの機能の等価性が確保されるほか、ファイナライザーのコード パスが引き続き呼び出されるようにできます。 言い換えると、DisposeAsyncCore() メソッドでは管理対象リソースが非同期的に破棄されるため、同期的にも破棄される必要はありません。 したがって、Dispose(true) ではなく Dispose(false) を呼び出します。

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() を呼び出し、フィールドを null に設定する、DisposeAsyncCore() メソッドが含まれています。
  • 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) オーバーロード メソッドでは、IDisposable インスタンスは、null でない場合に条件により破棄されます。 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 に関する FAQ」を参照してください。

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();
    }
}

1 行に複数の await キーワードがある場合

await キーワードが 1 行に複数回出現することがあります。 次に例を示します。

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

前の例の場合:

  • 次に、BeginTransactionAsync メソッドが待機しています。
  • 戻り値の型は DbTransaction であり、IAsyncDisposable を実装します。
  • transaction は、非同期的に使用され、これも待機します。

using の積み重ね

IAsyncDisposable を実装する複数のオブジェクトを作成して使用する場合、ConfigureAwaitawait using ステートメントを積み重ねると、誤った条件で DisposeAsync() を呼び出すことができなくなるおそれがあります。 常に DisposeAsync() が確実に呼び出されるようにするには、スタックを避ける必要があります。 次の 3 つのコード例に、代わりに使用できるパターンを示します。

使用可能なパターン 1


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 ブロックの下で明示的にスコープ設定されています。 外側のスコープは、objOneobjTwo を囲む中かっこを設定する方法に従っています。そのため、最初に objTwo が、その後に objOne が破棄されます。 どちらの IAsyncDisposable インスタンスでも DisposeAsync() メソッドを待機させているため、各インスタンスによって非同期のクリーンアップ操作が実行されます。 呼び出しは、積み重ねではなく、入れ子になっています。

使用可能なパターン 2

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 が破棄されます。

使用可能なパターン 3

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 が破棄されることを意味します。

許容できないパターン

次のコードの強調表示されている行は、"using の積み重ね" を持つことの意味を示しています。 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 ソース コードに関する説明を参照してください。