記述するほぼすべてのプログラムには、コレクションを反復処理する必要があります。 コレクション内のすべての項目を調べるコードを記述します。
また、反復子メソッドも作成します。反復子メソッドは、そのクラスの要素の 反復子 を生成するメソッドです。 反復子は、コンテナー (特にリスト) を走査するオブジェクトです。 反復子は次の目的で使用できます。
- コレクション内の各項目に対してアクションを実行する。
- カスタム コレクションの列挙。
- LINQ またはその他のライブラリの拡張。
- 反復子メソッドを介してデータが効率的に流れるデータ パイプラインを作成する。
C# 言語には、シーケンスの生成と使用の両方の機能が用意されています。 これらのシーケンスは、同期的または非同期的に生成および使用できます。 この記事では、これらの機能の概要について説明します。
foreach を使用した反復処理
コレクションの列挙は簡単です。 foreach
キーワードはコレクションを列挙し、コレクション内の各要素に対して埋め込みステートメントを 1 回実行します。
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
それだけです。
foreach
ステートメントだけでコレクションのすべての内容を反復処理できます。 ただし、 foreach
ステートメントは魔法ではありません。 .NET Core ライブラリで定義されている 2 つのジェネリック インターフェイスに依存して、コレクションを反復処理するために必要なコード ( IEnumerable<T>
と IEnumerator<T>
) を生成します。 このメカニズムについては、以下で詳しく説明します。
これらのインターフェイスには、 IEnumerable
と IEnumerator
という非ジェネリックなインターフェイスもあります。 最新のコードでは、 ジェネリック バージョンが推奨されます。
シーケンスが非同期的に生成される場合は、 await foreach
ステートメントを使用して、シーケンスを非同期的に使用できます。
await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}
シーケンスが System.Collections.Generic.IEnumerable<T>の場合は、 foreach
を使用します。 シーケンスが System.Collections.Generic.IAsyncEnumerable<T>の場合は、 await foreach
を使用します。 後者の場合、シーケンスは非同期的に生成されます。
反復子メソッドを使用した列挙ソース
C# 言語のもう 1 つの優れた機能を使用すると、列挙型のソースを作成するメソッドを構築できます。 これらのメソッドは 反復子メソッドと呼ばれます。 反復子メソッドは、要求されたときにシーケンス内のオブジェクトを生成する方法を定義します。
yield return
コンテキスト キーワードを使用して、反復子メソッドを定義します。
このメソッドを記述して、0 から 9 までの整数のシーケンスを生成できます。
public IEnumerable<int> GetSingleDigitNumbers()
{
yield return 0;
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
yield return 6;
yield return 7;
yield return 8;
yield return 9;
}
上のコードは、反復子メソッドで複数の個別のyield return
ステートメントを使用できることを強調する個別のyield return
ステートメントを示しています。 反復子メソッドのコードを簡略化するために、他の言語コンストラクトを使用できます (多くの場合に使用できます)。 以下のメソッド定義では、まったく同じ数のシーケンスが生成されます。
public IEnumerable<int> GetSingleDigitNumbersLoop()
{
int index = 0;
while (index < 10)
yield return index++;
}
いずれかを決定する必要はありません。 メソッドのニーズを満たすために必要な数の yield return
ステートメントを使用できます。
public IEnumerable<int> GetSetsOfNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
index = 100;
while (index < 110)
yield return index++;
}
上記の例はすべて、非同期に対応します。 いずれの場合も、 IEnumerable<T>
の戻り値の型を IAsyncEnumerable<T>
に置き換えます。 たとえば、前の例では、次の非同期バージョンがあります。
public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
int index = 0;
while (index < 10)
yield return index++;
await Task.Delay(500);
yield return 50;
await Task.Delay(500);
index = 100;
while (index < 110)
yield return index++;
}
これは、同期反復子と非同期反復子の両方の構文です。 実際の例を考えてみましょう。 IoT プロジェクトを使用していて、デバイス センサーが非常に大きなデータ ストリームを生成しているとします。 データの感覚を得るために、すべての N 番目のデータ要素をサンプリングするメソッドを記述できます。 この小さな反復子メソッドは、次のトリックを実行します。
public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
int index = 0;
foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}
IoT デバイスからの読み取りで非同期シーケンスが生成される場合は、次のメソッドに示すようにメソッドを変更します。
public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
int index = 0;
await foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}
反復子メソッドには 1 つの重要な制限があります。 return
ステートメントと yield return
ステートメントの両方を同じメソッドに含めることはできません。 次のコードはコンパイルされません。
public IEnumerable<int> GetSingleDigitNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
// generates a compile time error:
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
return items;
}
通常、この制限は問題ではありません。 メソッド全体で yield return
を使用するか、元のメソッドを複数のメソッドに分離するか、 return
を使用する方法と、 yield return
を使用する方法のいずれかを選択できます。
すべての場所で yield return
を使用するように、最後のメソッドを少し変更できます。
public IEnumerable<int> GetFirstDecile()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
foreach (var item in items)
yield return item;
}
場合によっては、反復子メソッドを 2 つの異なるメソッドに分割するのが正解です。 1 つは return
を使用し、もう 1 つは yield return
を使用します。 ブール型の引数を基に、空のコレクションまたは最初の 5 つの奇数を返す必要があるような場合を考えてみてください。 これは、次の 2 つのメソッドとして記述できます。
public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
if (getCollection == false)
return new int[0];
else
return IteratorMethod();
}
private IEnumerable<int> IteratorMethod()
{
int index = 0;
while (index < 10)
{
if (index % 2 == 1)
yield return index;
index++;
}
}
上記のメソッドを見てください。 1 つ目は、標準の return
ステートメントを使用して、空のコレクションまたは 2 番目のメソッドによって作成された反復子を返します。 2 番目のメソッドでは、 yield return
ステートメントを使用して、要求されたシーケンスを作成します。
foreach
に関する詳細な検討
foreach
ステートメントは、IEnumerable<T>
インターフェイスとIEnumerator<T>
インターフェイスを使用してコレクションのすべての要素を反復処理する標準的なイディオムに拡張されます。 また、リソースを適切に管理しないことで、開発者が行うエラーを最小限に抑えます。
コンパイラは、最初の例で示した foreach
ループを次のような構造に変換します。
IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
コンパイラによって生成される正確なコードはより複雑になり、 GetEnumerator()
によって返されるオブジェクトが IDisposable
インターフェイスを実装する状況を処理します。 完全な拡張では、次のようなコードが生成されます。
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}
コンパイラは、最初の非同期サンプルを次のコンストラクトに変換します。
{
var enumerator = collection.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}
列挙子の破棄方法は、 enumerator
の型の特性によって異なります。 一般的な同期の場合、 finally
句は次のように拡張されます。
finally
{
(enumerator as IDisposable)?.Dispose();
}
一般的な非同期ケースは次のように拡張されます。
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
ただし、 enumerator
の型がシール型であり、 enumerator
の型から IDisposable
または IAsyncDisposable
への暗黙的な変換がない場合、 finally
句は空のブロックに展開されます。
finally
{
}
enumerator
の型からIDisposable
への暗黙的な変換があり、enumerator
が null 非許容値型の場合、finally
句は次のように拡張されます。
finally
{
((IDisposable)enumerator).Dispose();
}
幸いなことに、これらすべての詳細を覚える必要はありません。
foreach
ステートメントは、これらのすべての微妙な違いを処理します。 コンパイラは、これらのコンストラクトのいずれかに対して正しいコードを生成します。
.NET