導入
非同期の戻り値は Task が基本ですが、「多くの場合すぐ答えが用意できる」処理では、毎回 Task を割り当てるのが無駄になります。ValueTask はその無駄を抑える軽量版。また、後片付け自体が非同期な資源には IAsyncDisposable と await using を使います。
図解
flowchart LR
A["ValueTask<T>"] -->|即答できる| S["Task を割り当てず値を返す(軽い)"]
A -->|待ちが要る| T["内部で Task を使う"]
style S fill:#e8f5e9
サンプル
// すぐ値を用意できるので、Task を割り当てずに返す
Console.WriteLine(await GetCachedAsync()); // 42
ValueTask<int> GetCachedAsync() => new ValueTask<int>(42);
// 後片付けが非同期な資源は IAsyncDisposable。await using で確実に閉じる
await using (var conn = new AsyncConnection())
{
await conn.SendAsync("hello");
}
class AsyncConnection : IAsyncDisposable
{
public async Task SendAsync(string msg)
{
await Task.Yield();
Console.WriteLine($"送信: {msg}");
}
public async ValueTask DisposeAsync()
{
await Task.Yield();
Console.WriteLine("接続を閉じた");
}
}
出力:
42
送信: hello
接続を閉じた
ValueTask<T>は「即答できることが多い非同期」でアロケーションを抑える(結果は一度だけ await する)IAsyncDisposableのDisposeAsyncに非同期の後片付けを書くawait usingはスコープを抜けるときDisposeAsyncを確実に呼ぶ
やってみよう
GetCachedAsync の中を await Task.Delay(1) してから値を返す形に変えても、呼び出し側 await GetCachedAsync() はそのまま動きます。ValueTaskが「即答も待ちも」表せることを確かめましょう。
演習
// TODO: ValueTask<int> を返し、100 を即座に返す GetScore を定義してください
___
Console.WriteLine(await GetScore()); // 100
- 期待される出力:
100
ヒント1を見る
ValueTask<int> GetScore() => new ValueTask<int>(100);
ヒント2を見る
待ちが不要なら new ValueTask<int>(値) で即座に返せます
まとめ
ValueTask<T>は「即答が多い非同期」の軽量版(結果の await は一度だけ)IAsyncDisposable+await usingで非同期の後片付けを確実に行う- どちらも高頻度な非同期処理でのアロケーション削減に効く
次回: 生産者と消費者をつなぐ Channel です(読み物)。