C# await foreachの使い方を基礎から解説!IAsyncEnumerableで非同期処理を扱う方法

はじめに

C#で非同期処理を書くとき、多くの場合はasyncawaitを使ってTaskを待機します。しかし、APIのページング取得、ファイルの逐次読み込み、データベースの結果処理、ストリーミングデータの受信など、「すべての結果がそろってから処理する」のではなく、「取得できたデータから順番に処理したい」場面もあります。

そのようなときに役立つのが、C#のawait foreachです。

await foreachを使うと、IAsyncEnumerable<T>で表される非同期ストリームから、データを1件ずつ非同期に取得できます。通常のforeachに似た書き方でありながら、各要素の取得を非同期に待てるため、大量データや時間のかかるデータ取得処理を効率よく扱えます。

この記事では、C#のawait foreachの基本から、IAsyncEnumerable<T>の仕組み、実践的な使い方、エラーの原因と解決方法までをわかりやすく解説します。

1. C#のawait foreachとは?非同期ストリームを扱う構文

await foreachは、C#で非同期に列挙できるデータを順番に処理するための構文です。通常のforeachは同期的なコレクションを対象にしますが、await foreachIAsyncEnumerable<T>のような非同期ストリームを対象にします。

たとえば、次のようなイメージです。

C#
await foreach (var item in GetItemsAsync())
{
Console.WriteLine(item);
}

このコードでは、GetItemsAsync()から返されるデータを1件ずつ非同期に取得し、取得できた順に処理します。

1-1. await foreachでできること

await foreachを使うと、時間をかけて順番に生成されるデータを、取得できたタイミングで処理できます。

たとえば、次のような処理に向いています。

C#
await foreach (var message in ReceiveMessagesAsync())
{
Console.WriteLine(message);
}

この例では、メッセージが届くたびに1件ずつ処理できます。すべてのメッセージを最初にまとめて取得する必要はありません。

await foreachでできる主なことは、非同期で要素を1件ずつ取得すること、取得した要素を順番に処理すること、途中でキャンセルや例外処理を組み合わせることです。

1-2. foreachとの違い

通常のforeachは、IEnumerable<T>などの同期的な列挙を扱います。

C#
foreach (var item in items)
{
Console.WriteLine(item);
}

一方、await foreachIAsyncEnumerable<T>などの非同期的な列挙を扱います。

C#
await foreach (var item in asyncItems)
{
Console.WriteLine(item);
}

大きな違いは、次の要素を取得するときに非同期処理を待てるかどうかです。

通常のforeachでは、次の要素の取得は同期的に行われます。要素を取得するために時間のかかるI/O処理がある場合、その間スレッドが待たされる可能性があります。

await foreachでは、次の要素の取得を非同期に待機できます。そのため、ネットワーク通信やファイル読み込みのようなI/O待ちが発生する処理に向いています。

1-3. Taskやasync/awaitとの関係

await foreachは、C#のasync/awaitと密接に関係しています。

通常の非同期メソッドでは、Task<T>を返します。

C#
public async Task<string> GetNameAsync()
{
await Task.Delay(1000);
return "Alice";
}

これは「非同期に1つの結果を返す」処理です。

一方、IAsyncEnumerable<T>は「非同期に複数の結果を順番に返す」処理を表します。

C#
public async IAsyncEnumerable<string> GetNamesAsync()
{
await Task.Delay(1000);
yield return "Alice";

await Task.Delay(1000);
yield return "Bob";
}

Task<T>が単発の非同期結果を扱うのに対し、IAsyncEnumerable<T>は複数の結果を非同期に流す仕組みです。その結果を受け取るために使うのがawait foreachです。

1-4. await foreachが必要になる場面

await foreachが必要になるのは、データを一括で取得するよりも、順番に処理したほうが効率的な場面です。

たとえば、APIからページ単位でデータを取得する場合、すべてのページを取得してから処理するより、1ページずつ取得しながら処理したほうがメモリ効率が良くなります。

また、大きなファイルを1行ずつ読み込む場合や、データベースから大量のレコードを処理する場合にも、await foreachは便利です。

C#
await foreach (var row in ReadLargeDataAsync())
{
Process(row);
}

このように、データの生成や取得に時間がかかる処理では、await foreachを使うことで効率的な非同期処理を書けます。

2. await foreachを使うための前提知識

await foreachを理解するには、async/awaitIEnumerable<T>IAsyncEnumerable<T>yield returnの基本を押さえておくとスムーズです。

2-1. async/awaitの基本

C#のasyncawaitは、非同期処理を読みやすく書くための仕組みです。

C#
public async Task ExecuteAsync()
{
await Task.Delay(1000);
Console.WriteLine("完了");
}

asyncを付けたメソッドでは、メソッド内でawaitを使えます。awaitは、非同期処理の完了を待ちますが、待機中にスレッドを占有し続けるわけではありません。

そのため、WebアプリケーションやGUIアプリケーションなどで、処理待ちによるブロッキングを避けるためによく使われます。

await foreachも同じように、非同期処理を待ちながらループを進めるための構文です。

2-2. IEnumerableとIEnumerable<T>の役割

IEnumerable<T>は、C#で「順番に列挙できるデータ」を表すインターフェースです。

たとえば、配列やList<T>IEnumerable<T>として扱えます。

C#
IEnumerable<int> numbers = new List<int> { 1, 2, 3 };

foreach (var number in numbers)
{
Console.WriteLine(number);
}

IEnumerable<T>は、foreachで1件ずつ値を取り出すための基本的な仕組みです。

ただし、IEnumerable<T>は同期的な列挙を前提としています。次の要素を取得する処理の中で非同期処理を自然に待つことはできません。

2-3. IAsyncEnumerable<T>とは

IAsyncEnumerable<T>は、非同期に列挙できるデータを表すインターフェースです。

IEnumerable<T>が同期的なデータ列挙を表すのに対し、IAsyncEnumerable<T>は非同期的なデータ列挙を表します。

C#
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 3; i++)
{
await Task.Delay(1000);
yield return i;
}
}

このメソッドは、1秒ごとに数値を1つずつ返します。

呼び出し側では、次のようにawait foreachで受け取ります。

C#
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number);
}

このように、IAsyncEnumerable<T>await foreachはセットで使われることが多いです。

2-4. yield returnと非同期処理の関係

yield returnは、値を1つずつ返すイテレーターを作るための構文です。

通常の同期イテレーターでは、次のように書きます。

C#
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}

非同期イテレーターでは、async IAsyncEnumerable<T>yield returnを組み合わせます。

C#
public async IAsyncEnumerable<int> GetNumbersAsync()
{
await Task.Delay(1000);
yield return 1;

await Task.Delay(1000);
yield return 2;
}

このように書くことで、値を返す前に非同期処理を実行できます。

2-5. 対応しているC#のバージョン

await foreachはC# 8.0で導入された機能です。そのため、C# 7.x以前のプロジェクトではそのまま使えません。

また、IAsyncEnumerable<T>は主に.NET Core 3.0以降や、対応する.NET環境で利用されます。

プロジェクトで使えない場合は、言語バージョンやターゲットフレームワークを確認しましょう。

XML
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>

または、C# 8.0以上を明示します。

XML
<PropertyGroup>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

3. await foreachの基本構文と使い方

ここからは、await foreachの基本構文と、IAsyncEnumerable<T>を返すメソッドの書き方を見ていきます。

3-1. await foreachの基本構文

await foreachの基本構文は次のとおりです。

C#
await foreach (var item in asyncItems)
{
// itemを処理する
}

asyncItemsには、IAsyncEnumerable<T>を返すメソッドやオブジェクトを指定します。

たとえば、次のように使います。

C#
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number);
}

このコードでは、GetNumbersAsync()から返される数値を、非同期に1件ずつ取得して表示します。

3-2. IAsyncEnumerableを返すメソッドの書き方

IAsyncEnumerable<T>を返すメソッドは、次のように書きます。

C#
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(500);
yield return i;
}
}

ポイントは、戻り値をIAsyncEnumerable<int>にすること、メソッドにasyncを付けること、値を返すときにyield returnを使うことです。

このメソッドは、500ミリ秒ごとに1から5までの数値を返します。

3-3. async iteratorの実装例

async IAsyncEnumerable<T>を返すメソッドは、非同期イテレーターと呼ばれます。

C#
public async IAsyncEnumerable<string> GetMessagesAsync()
{
await Task.Delay(1000);
yield return "1件目のメッセージ";

await Task.Delay(1000);
yield return "2件目のメッセージ";

await Task.Delay(1000);
yield return "3件目のメッセージ";
}

このメソッドは、1秒ごとにメッセージを1件ずつ返します。

呼び出し側は次のように書きます。

C#
await foreach (var message in GetMessagesAsync())
{
Console.WriteLine(message);
}

3-4. await foreachで値を順番に取得するサンプルコード

次のコードは、await foreachの最も基本的なサンプルです。

C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number);
}
}

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 3; i++)
{
await Task.Delay(1000);
yield return i;
}
}
}

このプログラムを実行すると、1秒ごとに123が表示されます。

GetNumbersAsync()はすべての値をまとめて返しているわけではありません。await foreachが次の値を要求するたびに、非同期処理を待ってから値を返します。

3-5. await foreach内で非同期処理を実行する方法

await foreachのループ内でも、通常のawaitを使えます。

C#
await foreach (var item in GetItemsAsync())
{
await SaveAsync(item);
}

このコードでは、GetItemsAsync()からデータを1件ずつ取得し、そのデータをSaveAsync()で非同期に保存しています。

ただし、この処理は基本的に順番に実行されます。1件目の保存が終わってから、次の要素の取得や処理に進みます。

順次処理で問題ない場合は非常に読みやすい書き方ですが、並列処理したい場合は別の設計が必要です。

4. IAsyncEnumerableで非同期データを扱う方法

await foreachを正しく理解するには、IAsyncEnumerable<T>の内部的な考え方も知っておくと役立ちます。

4-1. IAsyncEnumerable<T>の基本的な仕組み

IAsyncEnumerable<T>は、非同期に列挙できるデータの流れを表します。

同期的なIEnumerable<T>では、列挙子がMoveNext()を呼び出して次の要素に進みます。

一方、IAsyncEnumerable<T>では、非同期列挙子がMoveNextAsync()を呼び出して次の要素に進みます。

イメージとしては、次のような違いです。

C#
// 同期
foreach (var item in items)
{
Console.WriteLine(item);
}

// 非同期
await foreach (var item in asyncItems)
{
Console.WriteLine(item);
}

await foreachは、内部的には次の要素を取得するたびに非同期処理を待っています。

4-2. MoveNextAsyncとCurrentの役割

IAsyncEnumerable<T>では、内部的にIAsyncEnumerator<T>が使われます。

IAsyncEnumerator<T>には、主に次のメンバーがあります。

C#
ValueTask<bool> MoveNextAsync();
T Current { get; }

MoveNextAsync()は、次の要素があるかどうかを非同期に判定します。次の要素があればtrueを返し、なければfalseを返します。

Currentは、現在の要素を取得するプロパティです。

await foreachは、これらを直接書かなくても扱えるようにする構文です。

4-3. 非同期で1件ずつデータを返す流れ

await foreachでデータを処理すると、基本的には次のような流れになります。

まず、非同期列挙子を取得します。次に、MoveNextAsync()で次の要素を取得できるかを待ちます。取得できた場合はCurrentから値を取り出し、ループ本体を実行します。これを要素がなくなるまで繰り返します。

コードでイメージすると、次のような処理に近いです。

C#
await using var enumerator = GetNumbersAsync().GetAsyncEnumerator();

while (await enumerator.MoveNextAsync())
{
var number = enumerator.Current;
Console.WriteLine(number);
}

実際にはawait foreachを使えば、このような低レベルなコードを書く必要はありません。

4-4. ToListAsyncとの違い

非同期でデータを取得するとき、ToListAsync()のように結果をリストにまとめて取得する方法もあります。

C#
var items = await query.ToListAsync();

foreach (var item in items)
{
Console.WriteLine(item);
}

この方法では、すべてのデータを取得してから処理を開始します。

一方、await foreachでは、データを1件ずつ取得しながら処理できます。

C#
await foreach (var item in query.AsAsyncEnumerable())
{
Console.WriteLine(item);
}

ToListAsync()は、結果をまとめて使いたい場合に便利です。await foreachは、データを順次処理したい場合や、大量データをメモリにまとめて保持したくない場合に便利です。

4-5. メモリ効率が良くなる理由

await foreachがメモリ効率に優れている理由は、すべてのデータを一度にメモリへ読み込む必要がないためです。

たとえば、100万件のデータを処理する場合、List<T>にすべて格納してから処理すると、大量のメモリを消費します。

C#
var items = await GetAllItemsAsync();

foreach (var item in items)
{
Process(item);
}

一方、IAsyncEnumerable<T>を使えば、1件ずつ取得して処理できます。

C#
await foreach (var item in GetItemsAsync())
{
Process(item);
}

この方法では、処理中に必要なデータだけを保持すればよいため、大量データを扱う場合に有利です。

5. await foreachの実践的な利用例

ここでは、実務で使いやすいawait foreachの利用例を紹介します。

5-1. APIからデータを順次取得する例

APIがページング形式でデータを返す場合、IAsyncEnumerable<T>を使うと、ページごとに取得した結果を1件ずつ処理できます。

C#
public async IAsyncEnumerable<string> GetUsersFromApiAsync()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(500);

var users = new[]
{
$"User-{page}-1",
$"User-{page}-2"
};

foreach (var user in users)
{
yield return user;
}
}
}

呼び出し側は次のように書けます。

C#
await foreach (var user in GetUsersFromApiAsync())
{
Console.WriteLine(user);
}

ページ単位でAPIを呼び出しながら、取得できたユーザーを順番に処理できます。

5-2. ファイルを非同期で読み込む例

大きなファイルを1行ずつ読み込む場合にも、await foreachは便利です。

C#
public async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);

while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();

if (line is not null)
{
yield return line;
}
}
}

呼び出し側は次のように処理できます。

C#
await foreach (var line in ReadLinesAsync("sample.txt"))
{
Console.WriteLine(line);
}

ファイル全体を一度に読み込まず、1行ずつ処理できるため、大きなファイルでも扱いやすくなります。

5-3. データベースの結果を非同期に処理する例

Entity Framework Coreなどでは、クエリ結果を非同期ストリームとして扱える場面があります。

C#
await foreach (var user in dbContext.Users.AsAsyncEnumerable())
{
Console.WriteLine(user.Name);
}

この書き方を使うと、データベースから取得した結果を順番に処理できます。

ただし、データベース接続がループ中に維持される点には注意が必要です。ループ内で時間のかかる処理を行う場合は、接続時間やトランザクションの扱いも考慮しましょう。

5-4. 大量データを分割して処理する例

大量データを一括で取得せず、分割して処理したい場合にもIAsyncEnumerable<T>は役立ちます。

C#
public async IAsyncEnumerable<int> GetLargeDataAsync()
{
for (int i = 1; i <= 10000; i++)
{
if (i % 100 == 0)
{
await Task.Delay(10);
}

yield return i;
}
}

呼び出し側では、1件ずつ処理できます。

C#
await foreach (var value in GetLargeDataAsync())
{
Process(value);
}

このようにすることで、すべてのデータをメモリに保持せずに処理できます。

5-5. リアルタイムデータやストリーミング処理で使う例

チャットメッセージ、ログ監視、センサー値、キューからのメッセージ受信など、リアルタイムに近いデータ処理にもawait foreachは向いています。

C#
public async IAsyncEnumerable<string> WatchMessagesAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000);
yield return $"Message {i}";
}
}
C#
await foreach (var message in WatchMessagesAsync())
{
Console.WriteLine($"受信: {message}");
}

データが届いたタイミングで順次処理できるため、ストリーミング処理のコードを自然に書けます。

6. await foreachでよく使う書き方

await foreachでは、キャンセル、例外処理、ConfigureAwaitawait usingなどを組み合わせる場面があります。

6-1. await foreachとCancellationTokenを組み合わせる方法

長時間続く非同期ストリームでは、キャンセル処理が重要です。

C#
public async IAsyncEnumerable<int> GetNumbersAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 1; i <= 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

await Task.Delay(1000, cancellationToken);
yield return i;
}
}

CancellationTokenを受け取り、Task.Delayやループ内でキャンセルを確認します。

6-2. WithCancellationの使い方

呼び出し側では、WithCancellationを使ってCancellationTokenを渡せます。

C#
using var cts = new CancellationTokenSource();

await foreach (var number in GetNumbersAsync().WithCancellation(cts.Token))
{
Console.WriteLine(number);
}

WithCancellationを使うことで、await foreachの列挙処理にキャンセルを伝えられます。

ただし、呼び出し先のIAsyncEnumerable<T>側でキャンセルを受け取っていない場合や、キャンセルを無視している場合は、期待どおりに止まらないことがあります。

6-3. ConfigureAwaitを使う場合の書き方

ライブラリコードなどでは、ConfigureAwait(false)を使いたい場面があります。

await foreachでは、次のように書けます。

C#
await foreach (var item in GetItemsAsync().ConfigureAwait(false))
{
Console.WriteLine(item);
}

通常のawaitと同じく、コンテキストに戻る必要がない場合に使います。

アプリケーションコードでは必須ではありませんが、共通ライブラリを作る場合には検討することがあります。

6-4. try-catchで例外処理する方法

await foreachで発生する例外は、列挙中に発生することがあります。

C#
try
{
await foreach (var item in GetItemsAsync())
{
Console.WriteLine(item);
}
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}

IAsyncEnumerable<T>では、メソッドを呼び出した瞬間ではなく、実際に列挙したタイミングで処理が進みます。そのため、例外もawait foreachの実行中に発生することがあります。

6-5. await usingと組み合わせるケース

非同期で破棄が必要なリソースを扱う場合は、await usingを使います。

C#
await using var resource = new AsyncResource();

await foreach (var item in resource.GetItemsAsync())
{
Console.WriteLine(item);
}

await usingは、IAsyncDisposableを実装したオブジェクトを非同期に破棄するための構文です。

データベース接続、ストリーム、非同期リソースを扱う場合に使われることがあります。

7. await foreachを使うときの注意点

await foreachは便利ですが、いくつかの注意点があります。

7-1. IAsyncEnumerableでなければ使えない

await foreachを使えるのは、基本的にIAsyncEnumerable<T>のような非同期列挙に対応したオブジェクトです。

C#
IAsyncEnumerable<int> numbers = GetNumbersAsync();

await foreach (var number in numbers)
{
Console.WriteLine(number);
}

通常のList<T>や配列には使えません。

7-2. 通常のIEnumerableにはawait foreachを使えない

次のようなコードは正しくありません。

C#
IEnumerable<int> numbers = new[] { 1, 2, 3 };

await foreach (var number in numbers)
{
Console.WriteLine(number);
}

IEnumerable<T>は同期的な列挙なので、通常のforeachを使います。

C#
foreach (var number in numbers)
{
Console.WriteLine(number);
}

await foreachは、通常のforeachの完全な置き換えではありません。非同期列挙が必要な場合に使う構文です。

7-3. 並列処理ではなく順次処理である点

await foreachは、基本的に要素を順番に処理します。

C#
await foreach (var item in GetItemsAsync())
{
await ProcessAsync(item);
}

このコードでは、1件目のProcessAsyncが完了してから次の処理に進みます。

複数の処理を同時に実行したい場合は、Task.WhenAllや並列処理の仕組みを検討する必要があります。

7-4. 例外が発生するタイミングに注意する

IAsyncEnumerable<T>では、メソッドを呼び出した時点では処理が実行されないことがあります。実際の処理は、await foreachで列挙が始まったタイミングで進みます。

C#
var items = GetItemsAsync();

// この時点ではまだ処理が進んでいない場合がある

await foreach (var item in items)
{
Console.WriteLine(item);
}

そのため、例外もGetItemsAsync()の呼び出し時ではなく、ループ中に発生することがあります。

7-5. 無限ストリームやキャンセル処理に注意する

リアルタイムデータやメッセージ受信のような処理では、ストリームが終了しない場合があります。

C#
await foreach (var message in ReceiveMessagesAsync())
{
Console.WriteLine(message);
}

このような処理では、キャンセル手段を用意しないとループが終わらない可能性があります。

長時間動作するawait foreachでは、CancellationTokenを設計に含めることが重要です。

8. await foreachで発生しやすいエラーと解決方法

ここでは、await foreachでよくあるエラーと、その解決方法を紹介します。

8-1. foreach statement cannot operate on variables of typeの原因

foreach statement cannot operate on variables of typeというエラーは、対象の型がforeachまたはawait foreachで列挙できない場合に発生します。

たとえば、Task<List<T>>に対して直接await foreachしようとするとエラーになります。

C#
Task<List<int>> numbersTask = GetNumbersListAsync();

await foreach (var number in numbersTask)
{
Console.WriteLine(number);
}

この場合、まずawaitList<int>を取得し、その後通常のforeachを使います。

C#
var numbers = await GetNumbersListAsync();

foreach (var number in numbers)
{
Console.WriteLine(number);
}

await foreachを使うには、対象がIAsyncEnumerable<T>である必要があります。

8-2. GetAsyncEnumeratorが見つからない場合の対処法

GetAsyncEnumeratorが見つからないというエラーは、対象の型が非同期列挙に対応していない場合に発生します。

確認すべきポイントは、戻り値がIAsyncEnumerable<T>になっているか、必要な名前空間を追加しているか、C#のバージョンが対応しているかです。

C#
using System.Collections.Generic;

また、メソッドの戻り値がTask<IEnumerable<T>>になっていないかも確認しましょう。

C#
// await foreachの対象としては不適切
Task<IEnumerable<int>> GetNumbersAsync()

await foreachで使いたい場合は、次のようにします。

C#
async IAsyncEnumerable<int> GetNumbersAsync()

8-3. await foreachを使うメソッドにasyncが必要な理由

await foreachawaitを含む構文なので、通常はそれを使うメソッドにasyncが必要です。

C#
public async Task ExecuteAsync()
{
await foreach (var item in GetItemsAsync())
{
Console.WriteLine(item);
}
}

asyncを付けずにawait foreachを書くと、awaitを使えないためコンパイルエラーになります。

ただし、MainメソッドでもC#では非同期エントリポイントを使えます。

C#
public static async Task Main()
{
await foreach (var item in GetItemsAsync())
{
Console.WriteLine(item);
}
}

8-4. C#のバージョンが原因で使えない場合

await foreachはC# 8.0以降の機能です。古い言語バージョンを使っている場合、構文自体が認識されません。

プロジェクトファイルで言語バージョンを確認しましょう。

XML
<PropertyGroup>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

または、可能であれば最新の言語バージョンを指定します。

XML
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>

ターゲットフレームワークが古い場合も、IAsyncEnumerable<T>を自然に使えないことがあります。その場合は、.NETのバージョンや必要なパッケージを見直してください。

8-5. CancellationTokenが効かない場合の確認ポイント

WithCancellationを使っているのにキャンセルが効かない場合、呼び出し先がCancellationTokenを受け取っていない可能性があります。

C#
await foreach (var item in GetItemsAsync().WithCancellation(token))
{
Console.WriteLine(item);
}

呼び出し先では、次のようにCancellationTokenを受け取り、処理内で使用します。

C#
public async IAsyncEnumerable<int> GetItemsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();

await Task.Delay(1000, token);
yield return i;
}
}

キャンセルを有効にするには、トークンを渡すだけでなく、処理側でそのトークンを確認することが重要です。

9. await foreachと関連機能の違い

await foreachを正しく使い分けるために、関連する機能との違いを整理します。

9-1. await foreachとforeachの違い

foreachは同期的な列挙に使います。

C#
foreach (var item in items)
{
Console.WriteLine(item);
}

await foreachは非同期的な列挙に使います。

C#
await foreach (var item in asyncItems)
{
Console.WriteLine(item);
}

データがすでにメモリ上にある場合はforeachで十分です。データ取得に非同期処理が必要な場合はawait foreachが候補になります。

9-2. IAsyncEnumerableとIEnumerableの違い

IEnumerable<T>は同期的に値を列挙します。

C#
IEnumerable<int> GetNumbers()

IAsyncEnumerable<T>は非同期的に値を列挙します。

C#
IAsyncEnumerable<int> GetNumbersAsync()

IEnumerable<T>は、メモリ上のコレクションや同期的に生成できるデータに向いています。

IAsyncEnumerable<T>は、API、ファイル、データベース、ストリーミングなど、取得に非同期処理が必要なデータに向いています。

9-3. IAsyncEnumerableとTask<List<T>>の違い

Task<List<T>>は、非同期処理の結果としてリスト全体を返します。

C#
Task<List<User>> GetUsersAsync()

この場合、すべてのユーザーを取得してから呼び出し側に返します。

IAsyncEnumerable<T>は、非同期に1件ずつ値を返します。

C#
IAsyncEnumerable<User> GetUsersAsync()

結果をまとめて使いたい場合はTask<List<T>>が便利です。取得しながら順次処理したい場合はIAsyncEnumerable<T>が便利です。

9-4. IAsyncEnumerableとObservableの違い

IAsyncEnumerable<T>は、呼び出し側が次の値を取りに行くイメージの非同期ストリームです。await foreachで順番に値を取得します。

一方、IObservable<T>は、データの発生元が購読者に値を通知するプッシュ型の仕組みです。

IAsyncEnumerable<T>は、順次処理や非同期列挙に向いています。IObservable<T>は、イベント通知やリアクティブプログラミングに向いています。

単純にAPIやファイルから順番にデータを読むなら、IAsyncEnumerable<T>のほうが扱いやすいことが多いです。

9-5. await foreachとLINQの関係

通常のLINQは、主にIEnumerable<T>IQueryable<T>に対して使います。

C#
var result = items
.Where(x => x > 10)
.Select(x => x * 2);

IAsyncEnumerable<T>に対してLINQのような操作をしたい場合は、非同期ストリームに対応した拡張メソッドやライブラリを使うことがあります。

ただし、await foreachを使えば、LINQを使わなくても処理内容を明確に書けます。

C#
await foreach (var item in GetItemsAsync())
{
if (item > 10)
{
Console.WriteLine(item * 2);
}
}

シンプルな処理であれば、await foreachのほうが意図を読み取りやすい場合もあります。

10. await foreachを使うべきケース・使わないほうがよいケース

await foreachは便利ですが、すべての場面で使うべきではありません。用途に応じて、foreachTask<List<T>>、並列処理などと使い分けることが重要です。

10-1. await foreachが向いているケース

await foreachが向いているのは、データを非同期に少しずつ取得しながら処理したいケースです。

たとえば、APIからページング取得する処理、大きなファイルを読み込む処理、データベースの大量データを順次処理する処理、リアルタイムデータを受信する処理などです。

C#
await foreach (var item in GetPagedItemsAsync())
{
await ProcessAsync(item);
}

このような処理では、データを一括でメモリに読み込まずに済むため、メモリ効率が良くなります。

10-2. Task<List<T>>で十分なケース

取得するデータが少ない場合や、すべての結果をまとめて使いたい場合は、Task<List<T>>で十分です。

C#
var users = await GetUsersAsync();

foreach (var user in users)
{
Console.WriteLine(user.Name);
}

たとえば、画面に一覧表示するために数十件のデータを取得するだけなら、無理にIAsyncEnumerable<T>を使う必要はありません。

await foreachは便利ですが、設計が少し複雑になることもあります。単純な処理では、Task<List<T>>のほうがわかりやすい場合があります。

10-3. 並列処理が必要な場合の代替案

await foreachは基本的に順次処理です。複数の非同期処理を同時に実行したい場合は、Task.WhenAllなどを使います。

C#
var tasks = items.Select(item => ProcessAsync(item));
await Task.WhenAll(tasks);

ただし、大量のタスクを一度に作ると負荷が高くなるため、必要に応じて同時実行数を制限する設計が必要です。

await foreachで順番に取得しつつ、処理だけを一定数並列化するような設計もあります。

10-4. パフォーマンスを意識した使い分け

パフォーマンスを考えるときは、データ量、処理時間、メモリ使用量、I/O待ち時間を考慮します。

データ量が少なく、すべての結果がすぐ必要ならTask<List<T>>がわかりやすいです。

データ量が多く、1件ずつ処理できるならIAsyncEnumerable<T>await foreachが向いています。

各要素の処理が重く、同時に実行したいなら並列処理を検討します。

つまり、await foreachは「非同期に流れてくるデータを順番に処理する」ための選択肢です。

10-5. 実務で判断するときのポイント

実務で判断するときは、次の観点で考えるとわかりやすいです。

データをすべて保持する必要があるならTask<List<T>>、1件ずつ処理できるならIAsyncEnumerable<T>、すでにメモリ上にあるコレクションならforeach、同時に複数処理したいなら並列処理を検討します。

また、await foreachを使う場合は、キャンセル、例外処理、リソース解放も忘れずに設計しましょう。

11. await foreachの理解を深めるサンプルコード

最後に、await foreachの理解を深めるためのサンプルコードを紹介します。

11-1. 最小構成のサンプルコード

まずは最小構成のサンプルです。

C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
await foreach (var value in GetValuesAsync())
{
Console.WriteLine(value);
}
}

static async IAsyncEnumerable<int> GetValuesAsync()
{
yield return 1;
yield return 2;
yield return 3;

await Task.CompletedTask;
}
}

IAsyncEnumerable<int>を返すメソッドを定義し、await foreachで値を取得しています。

11-2. 遅延を入れて非同期処理を確認するサンプル

非同期で値が返される様子を確認するには、Task.Delayを入れるとわかりやすいです。

C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
await foreach (var value in GetValuesAsync())
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss} - {value}");
}
}

static async IAsyncEnumerable<int> GetValuesAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000);
yield return i;
}
}
}

このコードを実行すると、1秒ごとに値が表示されます。

11-3. キャンセル可能なawait foreachのサンプル

次は、CancellationTokenを使ってキャンセル可能にするサンプルです。

C#
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(3000);

try
{
await foreach (var value in GetValuesAsync().WithCancellation(cts.Token))
{
Console.WriteLine(value);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
}

static async IAsyncEnumerable<int> GetValuesAsync(
[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(1000, token);
yield return i;
}
}
}

CancelAfter(3000)により、約3秒後にキャンセルされます。

11-4. 例外処理を含むサンプル

await foreachの列挙中に例外が発生する例です。

C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
try
{
await foreach (var value in GetValuesAsync())
{
Console.WriteLine(value);
}
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}");
}
}

static async IAsyncEnumerable<int> GetValuesAsync()
{
yield return 1;
yield return 2;

await Task.Delay(500);
throw new InvalidOperationException("データ取得に失敗しました");
}
}

この例では、12を表示したあと、列挙中に例外が発生します。

11-5. 実務に近いデータ取得処理のサンプル

最後に、APIからページングでデータを取得するような実務に近いサンプルです。

C#
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
await foreach (var user in GetUsersAsync())
{
Console.WriteLine($"ユーザー処理: {user}");
}
}

static async IAsyncEnumerable<string> GetUsersAsync()
{
for (int page = 1; page <= 3; page++)
{
var users = await FetchUsersPageAsync(page);

foreach (var user in users)
{
yield return user;
}
}
}

static async Task<List<string>> FetchUsersPageAsync(int page)
{
await Task.Delay(1000);

return new List<string>
{
$"User-{page}-1",
$"User-{page}-2",
$"User-{page}-3"
};
}
}

このコードでは、ページ単位でデータを取得しながら、取得したユーザーを1件ずつ呼び出し側に返しています。

すべてのページを取得してから処理するのではなく、取得できたページから順次処理できる点がIAsyncEnumerable<T>のメリットです。

まとめ

C#のawait foreachは、IAsyncEnumerable<T>で表される非同期ストリームを順番に処理するための構文です。

通常のforeachが同期的なコレクションを扱うのに対し、await foreachは非同期に取得されるデータを1件ずつ処理できます。

Task<T>は非同期に1つの結果を返す仕組みですが、IAsyncEnumerable<T>は非同期に複数の結果を順番に返す仕組みです。そのため、APIのページング取得、大量データ処理、ファイル読み込み、データベース処理、ストリーミング処理などに向いています。

一方で、await foreachは並列処理ではなく順次処理である点、IAsyncEnumerable<T>でなければ使えない点、キャンセルや例外処理を適切に設計する必要がある点には注意が必要です。

少量のデータをまとめて取得するだけならTask<List<T>>で十分な場合もあります。大量データを効率よく処理したい場合や、データを取得しながら順番に処理したい場合には、await foreachIAsyncEnumerable<T>を活用すると、読みやすく効率的なC#の非同期処理を書けます。