概要
コレクションの要素の列挙・反復の方法には、 内部イテレータと呼ばれる方式と外部イテレータと呼ばれる2つの方式(デザインパターン)があります。
ちなみに、列挙(enumerate)と反復(iterate)ってのは、 この分野において、 「コレクション内の全要素に対して処理する」って意味ではほぼ同義語です。 ただし、 C++ の iterator が順方向・逆方向・双方向・ランダムアクセス可能なのに対して、 C# の Enumerator が順方向アクセスしかできないので、 そのイメージに引きずられて意味を使い分ける場合もあります。
内部イテレータと外部イテレータ
百聞は一見にしかずということで、具体例を出しつつ内部イテレータと外部イテレータについて説明しましょう。
例ということで、余計な機能は一切省いた以下のような低機能リストを考えます。
public class List
{
int[] items;
public List(params int[] items)
{
this.items = items;
}
後略
}
で、この items の中身を列挙したい場合に、アプローチが2つあると。
内部イテレータ
1つ目が内部イテレータ。 まず、List クラス内に以下のようなメソッドを用意。
public delegate void ForEachAction(int x);
public class List
{
前略
/// <summary>
/// 内部イテレータ的に、
/// リストの各要素にたいして action を適用する。
/// </summary>
/// <param name="action">適用したい動作</param>
public void ForEach(ForEachAction action)
{
for (int i = 0; i < this.items.Length; ++i)
{
action(this.items[i]);
}
}
}
で、以下のようにして使います。
List l = new List(1, 2, 3, 4, 5);
int sum = 0;
l.ForEach(delegate(int x)
{
sum += x;
}
);
Console.Write("sum = {0}\n", sum);
要するに、反復の仕方は List クラスの中の、ForEach メソッドの中に書いて、 要素ごとに行いたい処理をデリゲートとして渡します。
ForEach の実装は簡単なんですけども、 利用側がちょっと美しくないです。 また、この方法だと、break とか continue が使えなかったりします。
外部イテレータ
もう1つが外部イテレータ。 .NET Framework がとってるアプローチはこちらです。
こちらは、実装がちょっと面倒になります。 (C# 2.0 からの新機能である「イテレーター」を使えば簡単に書けるようになりますが、ここでは説明ということで IEnumerator を自前で実装します。)
public class List
{
前略
/// <summary>
/// 外部イテレータ用の IEnumerator 実装クラス。
/// </summary>
class Enumerator : IEnumerator<int>
{
List l;
int n;
internal Enumerator(List l)
{
this.l = l;
this.n = -1;
}
public int Current
{
get { return l.items[n]; }
}
void IDisposable.Dispose() { }
object System.Collections.IEnumerator.Current
{
get { return this.Current; }
}
bool System.Collections.IEnumerator.MoveNext()
{
++n;
return n != l.items.Length;
}
void System.Collections.IEnumerator.Reset()
{
n = -1;
}
}
/// <summary>
/// 外部イテレータを返す。
/// </summary>
/// <returns>イテレータ</returns>
public IEnumerator<int> GetEnumerator()
{
return new Enumerator(this);
}
}
で、利用側は以下のような感じ。
List l = new List(1, 2, 3, 4, 5);
int sum = 0;
IEnumerator<int> e = l.GetEnumerator();
while (e.MoveNext())
{
sum += e.Current;
}
Console.Write("sum = {0}\n", sum);
要するに、Enumerator という別のクラスを通して items 中の要素を1つずつ取り出します。
MoveNext とか Current とかいちいち書くのが面倒ではありますが、 while を使っていて、この方が反復処理らしくはあります。 あと、ちゃんと break や continue も使えます。
ただ、IEnumerator を実装するのがものすごい面倒な作業になります。
( あと、内部イテレータの方では、ループ1回に付き、action デリゲートが1回呼ばれるだけなのに対して、 こちらの場合はループ1回に付き MoveNext と Current (の getter)という2回のメソッド呼び出しがあります。 なので、こっちのアプローチの方が、実はほんのちょっとだけ遅い。)
C# の foreach
前節のとおり、外部イテレータ的アプローチには2つ面倒なところがあります。
-
MoveNext とか Current とかいちいち書くのがめんどくさい
-
IEnumerator の実装がものすごい面倒
このうち、1つ目の面倒ごとを解決してくれるのが、C# の foreach 文です。 実は、前節の時点ですでに、List クラスに foreach 文を使うために必要なコードの大半を書いているので、 あとは、以下のように、IEnumerable インターフェースを実装するだけです。
public class List : IEnumerable<int>
{
前略
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
return this.GetEnumerator();
}
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
これで、C# の List クラスの要素を foreach 文で列挙できるようになります。
List l = new List(1, 2, 3, 4, 5);
int sum = 0;
foreach (int x in l)
{
sum += x;
}
Console.Write("sum = {0}\n", sum);
まあ、利用側の見た目をすっきりさせるための構文糖(便法)みたいなもんで、 コンパイル時に、 while (e.MoveNext()) に相当するコードに変換されます。
C# のイテレータ構文
外部イテレータの2つ目の面倒ごと、すなわち、 「IEnumerator の実装がものすごい面倒」という問題を解決するために導入されたのが、 C# 2.0 の「イテレーター」構文です。
「外部イテレータ」で示した GetEnumerator メソッドを、 以下のように書き換えることで、 Enumerator クラスに相当するものを自動的に生成してくれます。
/// <summary>
/// イテレータ構文を使って外部イテレータを自動生成。
/// </summary>
/// <returns>イテレータ</returns>
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < this.items.Length; ++i)
{
yield return this.items[i];
}
}
見た目的には、 「内部イテレータ」で書いた Foreach メソッドとほとんど一緒です。 (action デリゲート呼び出しの部分が yield return に変わっただけ。)
要するに、C# 2.0 のイテレータ構文というのは、 内部イテレータ的な書き方で、 外部イテレータを自動生成するものです。 (なので、他の言語では、 これと似たような構文のことをジェネレータ(genrator: 生成するもの)と呼んだりします。 )