【C#】イテレーターの仕組み(コンパイラ展開シミュレート含みます)
イテレーター(反復)動作についてC#では、仕組みが用意されています。
ユーザーは、簡易に使えるように内部で処理をされていますが、今回は、どのようにコードがコンパイラで展開されているのかをみてみましょう
yield returnを2行のみ
ユーザー作成コード
foreach (var item in Collection())
{
Console.WriteLine(item);
}
IEnumerable<int> Collection()
{
yield return 1;
yield return 2;
}
実行結果
仕組みを理解するためにブレークポイントを設定して、ステップ実行でコードの進行を確認してみましょう
上記のコードを実行すると、Collection
メソッド(またはコンパイラによって展開されたCollection
クラス)から生成されるイテレータを通じて反復処理が行われます。このイテレータは、2つの整数値 1
と 2
を順に生成し、それらをコンソールに出力します。
具体的には、実行結果は以下のようになります:
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
ループの展開は、GetEnumerator
、MoveNext
、そしてCurrent
プロパティの呼び出しを直接使ってループを実行する形になります。using
ステートメントは、IEnumerator<int>
がIDisposable
を実装している場合に、ループの終了後にリソースを適切に解放するために使用されます。
ディスカッション
コメント一覧
まだ、コメントがありません