【C#】イテレーターの仕組み(コンパイラ展開シミュレート含みます)

2024年4月2日

イテレーター(反復)動作についてC#では、仕組みが用意されています。
ユーザーは、簡易に使えるように内部で処理をされていますが、今回は、どのようにコードがコンパイラで展開されているのかをみてみましょう

yield returnを2行のみ

ユーザー作成コード

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

IEnumerable<int> Collection()
{
    yield return 1;
    yield return 2;
}

実行結果

仕組みを理解するためにブレークポイントを設定して、ステップ実行でコードの進行を確認してみましょう

上記のコードを実行すると、Collectionメソッド(またはコンパイラによって展開されたCollectionクラス)から生成されるイテレータを通じて反復処理が行われます。このイテレータは、2つの整数値 12 を順に生成し、それらをコンソールに出力します。

具体的には、実行結果は以下のようになります:

1
2

この結果は、Collectionメソッド内のyield return文が2回実行され、最初に1が、次に2が返されることを示しています。foreachループはこれらの値を受け取り、それぞれの値に対してConsole.WriteLineメソッドを呼び出して、値をコンソールに出力します。

このシンプルな例では、yield returnを使ったイテレーションのメカニズムがどのように機能するか、そしてコンパイラがどのようにこのメカニズムをIEnumerable<T>およびIEnumerator<T>を実装するクラスに展開するかが示されています。

コンパイラが展開したコード

yield returnを使ったイテレータメソッドのコンパイラ展開をクラスレベルで説明すると、C#コンパイラはyield returnを含むメソッドを一種の状態マシンに変換します。この状態マシンは、IEnumerable<T>およびIEnumerator<T>インターフェイスを実装するクラスとして構築されます。ここでは、Collectionメソッドがどのように展開されるかを具体的に見ていきましょう。

Collectionメソッドの簡易版のコンパイラ展開例を示します。このコードは、コンパイラが内部的に生成するコードの概念的な理解を目的としており、実際のコンパイラ出力とは異なる場合がありますが、yield returnの背後にあるメカニズムを理解するのに役立ちます。

using System.Collections;

class Program
{
    static void Main(string[] args)
    {
        foreach (var item in new Collection())
        {
            Console.WriteLine(item);
        }
    }
}

// コンパイラによって生成されるイテレータの概念的な展開
public class Collection : IEnumerable<int>, IEnumerator<int>
{
    private int state = 0;
    private int current;

    public int Current => current;

    object IEnumerator.Current => Current;

    public Collection() { }

    public bool MoveNext()
    {
        switch (state)
        {
            case 0:
                current = 1;
                state = 1;
                return true;
            case 1:
                current = 2;
                state = 2;
                return true;
            case 2:
                return false;
            default:
                return false;
        }
    }

    public void Reset()
    {
        throw new NotImplementedException();
    }

    public void Dispose() { }

    public IEnumerator<int> GetEnumerator()
    {
        // 通常、新しいイテレーションごとに新しいイテレータインスタンスを返す必要があります。
        // しかし、ここではシンプルさのために、このインスタンス自身を返します。
        // 実際の展開では、各GetEnumerator呼び出しで新しいインスタンスが生成されます。
        return this;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

yield returnをfor文で繰り返す

ユーザー作成コード

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

IEnumerable<int> Collection()
{
    for (int i = 0; i < 10; i++)
    {
        yield return i;
    }
}

実行結果

上記のコードを実行すると、コンソールに0から9までの数字が一行ずつ出力されます。Collectionメソッドは、0から9までの整数を生成し、それぞれの数値がforeachループによって取り出され、Console.WriteLine(item);によってコンソールに表示されます。

具体的な実行結果は以下の通りです:

0
1
2
3
4
5
6
7
8
9

この出力は、Collectionメソッドがイテレータパターンを利用して内部的に状態を管理しながら、0から始まり9で終わる連続した整数を順番に生成し、それらがforeachループによって一つずつ取り出されていることを示しています。Console.WriteLine(item);はこれらの各数値をコンソールに出力します。

コンパイラが展開したコード

foreachループとyield returnを使用するコードをコンパイラがどのように展開するかのイメージを示すには、まずforeachループが内部的にどのように動作するかを理解する必要があります。yield returnを使用すると、C# コンパイラは自動的に状態マシンを生成します。この状態マシンは、イテレーションごとに次の値を返す方法を管理します。

以下は、指定されたコードがコンパイラによってどのように展開されるかのイメージです。クラスの展開も含めています。

using System;
using System.Collections;
using System.Collections.Generic;

internal class Program
{
    private static void Main(string[] args)
    {
        // コンパイラがforeachループを展開した形
        using (IEnumerator<int> enumerator = Collection().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var item = enumerator.Current;
                Console.WriteLine(item);
            }
        }
    }

    private static IEnumerable<int> Collection()
    {
        return new CollectionEnumerable();
    }

    // コンパイラが 'yield return' を使用するメソッドから生成するクラス
    private class CollectionEnumerable : IEnumerable<int>, IEnumerator<int>
    {
        private int state = 0;
        private int current;

        public int Current => current;

        object IEnumerator.Current => Current;

        public CollectionEnumerable() { }

        public bool MoveNext()
        {
            switch (state)
            {
                case 0:
                    state = 1;
                    current = -1;
                    break;
                case 1:
                    break;
                default:
                    return false;
            }

            if (current < 9)
            {
                current++;
                return true;
            }

            state = -1; // イテレーションの終了
            return false;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            // 必要に応じてリソース解放
        }

        public IEnumerator<int> GetEnumerator()
        {
            if (state == 0 || state == -1)
            {
                state = 0;
                return this;
            }
            else
            {
                return new CollectionEnumerable();
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

この例では、Collectionメソッドが生成するIEnumerable<int>IEnumerator<int>の実装を含むCollectionEnumerableクラスを手動で作成しています。実際には、コンパイラがyield returnを使用するメソッドから生成するクラスは、このように明示的に書かれることはなく、より複雑な命名や追加の最適化が施されることがありますが、基本的な概念としてはこのように理解できます。

コンパイラによるforeachループの展開は、GetEnumeratorMoveNext、そしてCurrentプロパティの呼び出しを直接使ってループを実行する形になります。usingステートメントは、IEnumerator<int>IDisposableを実装している場合に、ループの終了後にリソースを適切に解放するために使用されます。

C#,Unity

Posted by hidepon