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()
メソッドを定義する必要があります。
パラメーターを持たない
public
IAsyncDisposable.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()
メソッドを呼び出し、含まれているフィールドを使ってパターンを実装します (破棄のカスケード)。
破棄と非同期の破棄の両方のパターンを実装する
場合によっては、IDisposable と IAsyncDisposable の両方のインターフェイスを実装する必要があります。クラスのスコープにこれらの実装のインスタンスが含まれている場合は特にそうです。 こうすることで、クリーンアップの呼び出しを適切に連鎖させることができます。 次に、両方のインターフェイスを実装し、クリーンアップの適切なガイダンスを示すクラスの例を示します。
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 を実装する複数のオブジェクトを作成して使用する場合、ConfigureAwait で await 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
ブロックの下で明示的にスコープ設定されています。 外側のスコープは、objOne
が objTwo
を囲む中かっこを設定する方法に従っています。そのため、最初に 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
ステートメントがスタックされていないときに正しく実行されます。
関連項目
IDisposable
と IAsyncDisposable
を両方実装する例については、GitHub の Utf8JsonWriter ソース コードに関する説明を参照してください。
.NET