2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

using × Task<T> に潜むバグとその防ぎ方

Last updated at Posted at 2025-05-28

Task<IDisposable>を返すとリソース解放漏れがおきやすい

public async Task<FileStream> GetFileStreamAsync() { ... }

async Task AMethodAsync()
{
    // 警告されないawait忘れ
    using var stream = GetFileStreamAsync();
    ...
}

このコードの問題点は、GetFileStreamAsync()の呼び出し時にawaitを付けていないため、FileStreamDispose()されないことです。代わりにTask<FileStream>Dispose()されてしまいます。

Task<TResult>自体がIDisposableを実装しているために、コンパイラは警告を出せない。このため、このバグは気づかれにくくなります。

このようなリソース解放漏れを引き起こしやすいAPIの採用は、極力避けるべきです。
この投稿では、その回避方法について説明します。

方法1: ValueTask<TResult>を使用する

usingでタスクの結果を使うことが想定されるメソッドでは、Task<TResult>の代わりに ValueTask<TResult>を返すことで、この問題を防げます。

ValueTask<TResult>IDisposableを実装していないため、間違えてawaitを付けずに使った場合、コンパイルエラーになります。

public async ValueTask<FileStream> GetFileStreamAsync() { ... }

async Task AMethodAsync()
{
    using var stream = await GetFileStreamAsync();
                    // ^^^^^ これがないとコンパイルエラー
    ...
}

これでこの類のリソースの解放漏れは防げます。
ただし、ValueTask<TResult>には以下のような制約があります:

  • awaitを複数回使えない
  • AsTaskを複数回呼び出せない
  • 未完了の状態でResultGetAwaiter().GetResult()を呼ぶと未定義動作
  • 上記のプロパティやメソッドを複数回使うと同様に未定義動作
  • WhenAllWhenAnyに直接渡せない

とはいえ、タスクを同期的に扱う範囲では、実用上大きな問題はありません。

方法2: AsyncExライブラリのAwaitableDisposable<T>を使用する

Stephen Cleary 氏のAsyncExライブラリでは、AwaitableDisposable<T>という型を用いてこの問題に対処しています。

この型の特徴は以下の通りです:

  • Task<TResult>をラップして返す (TResultIDisposableを実装)
  • 自身はIDisposableを実装しない
  • awaitすると内部のTResultを返す

詳細は このStackOverflowの回答が参考になります。

この仕組みを使うと、以下のように書けます。

public AwaitableDisposable<FileStream> GetFileStreamAsync() { ... }

async Task AMethodAsync()
{
    using var stream = await GetFileStreamAsync();
                    // ^^^^^ これがないとコンパイルエラー
    ...
}

この型は自作しなくても、Nito.AsyncExパッケージを使えば簡単に導入できます。

ValueTask<TResult>を使っていないので、前述の未定義動作の懸念もなくなります。
「未定義動作? 地球が爆発するかもしれない!こわい!」という方にも安心です。

そもそも、なぜTask<TResult>IDisposableを実装しているのか

TaskDispose()しているコードなんて見たことない」という方も多いと思います。
実際、通常の使用ではDispose()は不要です。

とはいえ、極めてまれにDispose()すべきケースが存在します。

詳細は公式ブログにて:

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?