IteratorsIterators

プログラムを記述するうえで、ほとんどのプログラムに必要になるのがコレクションの反復処理です。Almost every program you write will have some need to iterate over a collection. 反復処理が必要な場合は、コレクション内のすべての項目を調べるコードを記述します。You'll write code that examines every item in a collection.

また、クラスの要素に対して反復子を生成するメソッドである、反復子メソッドも作成することになります。You'll also create iterator methods which are methods that produces an iterator for the elements of that class. 反復子メソッドは以下のような目的に使用できます。These can be used for:

  • コレクション内の各項目に対するアクションの実行。Performing an action on each item in a collection.
  • カスタム コレクションの列挙。Enumerating a custom collection.
  • LINQ やその他のライブラリの拡張。Extending LINQ or other libraries.
  • 反復子メソッドによってデータ フローを効率化するデータ パイプラインの作成。Creating a data pipeline where data flows efficiently through iterator methods.

C# 言語には、上記の両方のシナリオに対応するための機能が用意されています。The C# language provides features for both these scenarios. この記事では、それらの機能の概要について説明します。This article provides an overview of those features.

このチュートリアルには、複数の手順があります。This tutorial has multiple steps. 各手順の後に、アプリケーションを実行して進行状況を確認できます。After each step, you can run the application and see the progress. このトピックの完全なサンプルを表示またはダウンロードすることもできます。You can also view or download the completed sample for this topic. ダウンロード方法については、「サンプルおよびチュートリアル」を参照してください。For download instructions, see Samples and Tutorials.

foreach を使用した反復処理Iterating with foreach

コレクションの列挙処理は単純です。foreach キーワードによってコレクション内の要素ごとに埋め込みステートメントを 1 回実行し、コレクションを列挙します。Enumerating a collection is simple: The foreach keyword enumerates a collection, executing the embedded statement once for each element in the collection:

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

これで完成です。That's all there is to it. foreach ステートメントさえあれば、コレクションに含まれるすべての内容を反復処理できます。To iterate over all the contents of a collection, the foreach statement is all you need. ただし、foreach ステートメントは魔法ではありません。The foreach statement isn't magic, though. コレクションの反復処理に必要なコードを生成するためには、.NET コア ライブラリに定義されている 2 つのジェネリック インターフェイス、IEnumerable<T>IEnumerator<T> が不可欠です。It relies on two generic interfaces defined in the .NET core library in order to generate the code necessary to iterate a collection: IEnumerable<T> and IEnumerator<T>. このメカニズムについては、後ほど詳しく説明します。This mechanism is explained in more detail below.

これら 2 つのインターフェイスに対応する非ジェネリック インターフェイスとして、IEnumerableIEnumerator があります。Both of these interfaces also have non-generic counterparts: IEnumerable and IEnumerator. 最新のコード向けにはジェネリック バージョンが適しています。The generic versions are preferred for modern code.

反復子メソッドを使用した列挙型のソースEnumeration sources with iterator methods

C# 言語のもう 1 つの優れた機能を利用することで、列挙型用のソースを作成するメソッドを構築できます。Another great feature of the C# language enables you to build methods that create a source for an enumeration. このようなメソッドを、"反復子メソッド" と呼びます。These are referred to as iterator methods. 反復子メソッドでは、要求があった場合にオブジェクトがどのようなシーケンスで生成されるかを定義します。An iterator method defines how to generate the objects in a sequence when requested. 反復子メソッドを定義するには、yield return コンテキスト キーワードを使用します。You use the yield return contextual keywords to define an iterator method.

次のメソッドを記述することで、0 ~ 9 の整数からなるシーケンスを生成できます。You could write this method to produce the sequence of integers from 0 through 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 ステートメントを示しています。The code above shows distinct yield return statements to highlight the fact that you can use multiple discrete yield return statements in an iterator method. 反復子メソッドのコードを簡略化するために、他の言語構成要素を使用することができます (実際、頻繁に使用します)。You can (and often do) use other language constructs to simplify the code of an iterator method. 次のメソッド定義では、まったく同じ数値のシーケンスが生成されます。The method definition below produces the exact same sequence of numbers:

public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index++ < 10)
        yield return index;
}

どちらか一方に決める必要はありません。You don't have to decide one or the other. メソッドのニーズに合わせて必要な数だけ yield return ステートメントを使用できます。You can have as many yield return statements as necessary to meet the needs of your method:

public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index++ < 10)
        yield return index;

    yield return 50;

    index = 100;
    while (index++ < 110)
        yield return index;
}

これが基本的な構文です。That's the basic syntax. 反復子メソッドを記述することになるであろう実際の例について考えてみましょう。Let's consider a real world example where you would write an iterator method. 自分が IoT プロジェクトに参加しているとして、デバイス センサーから膨大な量のデータ ストリームが生成されている状況を想像してください。Imagine you're on an IoT project and the device sensors generate a very large stream of data. データをおおまかに把握するためには、N 番目ごとにデータ要素をサンプリングするメソッドを記述することになります。To get a feel for the data, you might write a method that samples every Nth data element. このような処理は、次の小さな反復子メソッドで実現できます。This small iterator method does the trick:

public static IEnumerable<T> Sample(this IEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

反復子メソッドには重要な制限事項が 1 つあり、return ステートメントと yield return ステートメントの両方を同じメソッド内で使用することはできません。There is one important restriction on iterator methods: you can't have both a return statement and a yield return statement in the same method. そのため、次のコードはコンパイルされません。The following will not compile:

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;
}

通常は、この制限が問題になることはありません。This restriction normally isn't a problem. メソッド全体で yield return を使用するか、元のメソッドを複数に分割して一部のメソッドでは return、一部のメソッドでは yield return を使用するか、いずれかの方法を選択できます。You have a choice of either using yield return throughout the method, or separating the original method into multiple methods, some using return, and some using yield return.

前のメソッドを少し修正すると、メソッド全体で yield return のみを使用するように変更できます。You can modify the last method slightly to use yield return everywhere:

public IEnumerable<int> GetSingleDigitNumbers()
{
    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 つの異なるメソッドに分割することが正解となる場合もあります。Sometimes, the right answer is to split an iterator method into two different methods. つまり、return を使用するメソッドと yield return を使用するメソッドの 2 つです。One that uses return, and a second that uses yield return. ブール型の引数を基に、空のコレクションまたは最初の 5 つの奇数を返す必要があるような場合を考えてみてください。Consider a situation where you might want to return an empty collection, or the first 5 odd numbers, based on a boolean argument. この処理は、次の 2 つのメソッドとして記述できます。You could write that as these two methods:

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;
}

上記のメソッドを見てみましょう。Look at the methods above. 1 つ目のメソッドでは、標準の return ステートメントを使用して空のコレクションまたは 2 つ目のメソッドで作成された反復子のいずれかを返します。The first uses the standard return statement to return either an empty collection, or the iterator created by the second method. 2 つ目のメソッドでは、yield return ステートメントを使用して要求されたシーケンスを作成します。The second method uses the yield return statement to create the requested sequence.

foreach の詳細Deeper Dive into foreach

foreach ステートメントは、IEnumerable<T> および IEnumerator<T> インターフェイスを使用してコレクションの全要素を反復処理する標準的な表現形式に展開されます。The foreach statement expands into a standard idiom that uses the IEnumerable<T> and IEnumerator<T> interfaces to iterate across all elements of a collection. また、開発者の不適切なリソース管理によって生じるエラーを最小化する効果もあります。It also minimizes errors developers make by not properly managing resources.

最初の例に登場する foreach ループは、コンパイラによって次のコンストラクトに似たコードに変換されます。The compiler translates the foreach loop shown in the first example into something similar to this construct:

IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

上記のコンストラクトは、バージョン 5 以降の C# コンパイラによって生成されるコードを表しています。The construct above represents the code generated by the C# compiler as of version 5 and above. バージョン 5 より前のバージョンでは、item 変数のスコープが異なります。Prior to version 5, the item variable had a different scope:

// C# versions 1 through 4:
IEnumerator<int> enumerator = collection.GetEnumerator();
int item = default(int);
while (enumerator.MoveNext())
{
    item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

この点が変更された理由は、以前の動作に、ラムダ式に関連する微妙なバグや診断の難しいバグを発生させる可能性があったためです。This was changed because the earlier behavior could lead to subtle and hard to diagnose bugs involving lambda expressions. ラムダ式について詳しくは、「ラムダ式」をご覧ください。For more information about lambda expressions, see Lambda expressions.

コンパイラによって実際に生成されるコードはもう少し複雑であり、GetEnumerator() から返されるオブジェクトで IDisposable インターフェイスを実装する場合の処理も含まれています。The exact code generated by the compiler is somewhat more complicated, and handles situations where the object returned by GetEnumerator() implements the IDisposable interface. 全展開によって生成されるコードは、次のようになります。The full expansion generates code more like this:

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    } finally
    {
        // dispose of enumerator.
    }
}

列挙子が破棄される場合、その方法は enumerator の型の特性によって異なります。The manner in which the enumerator is disposed of depends on the characteristics of the type of enumerator. 一般的なケースでは、finally 句は次のように展開されます。In the general case, the finally clause expands to:

finally
{
   (enumerator as IDisposable)?.Dispose();
}

ただし、enumerator の型がシール型で、enumerator から IDisposable への暗黙的な型変換がない場合、finally 句は空のブロックに展開されます。However, if the type of enumerator is a sealed type and there is no implicit conversion from the type of enumerator to IDisposable, the finally clause expands to an empty block:

finally
{
}

enumerator から IDisposable への暗黙的な型変換があり、enumerator が null 非許容の値型である場合、finally 句は次のように展開されます。If there is an implicit conversion from the type of enumerator to IDisposable, and enumerator is a non-nullable value type, the finally clause expands to:

finally
{
   ((IDisposable)enumerator).Dispose();
}

さいわいなことに、これらの詳細をすべて覚えておく必要はありません。Thankfully, you don't need to remember all these details. このような微妙な差異は、いずれも foreach ステートメントによって処理されます。The foreach statement handles all those nuances for you. コンパイラでは、これらすべてのコンストラクトに対して正しいコードが生成されます。The compiler will generate the correct code for any of these constructs.