C# の foreach 構文は、コレクションクラスの利用者側から見ると非常に便利な機能です。
しかしながら、実装側から見た場合、IEnumerableやIEnumeratorインターフェースを実装する必要があり、結構面倒な作業が必要でした。
この実装側の労力を軽減するために、C# 2.0ではイテレータ構文というものが追加されました。
クラス中にイテレータブロック(iterator block)と言うものを定義することで、foreach 文で利用可能なコレクションクラスを簡単に実装することができます。 イテレータブロックは、以下のように、一見すると通常のメソッドと似ています。
using System.Collections.Generic; class TestEnumerable { // ↓これがイテレータブロック。IEnubrable を実装するクラスを自動生成してくれる。 static public IEnumerable<int> FromTo(int from, int to) { while(from <= to) yield return from++; } static void Main(string[] args) { // ↓こんな感じで使う。 foreach(int i in FromTo(10, 20)) { Console.Write("{0}\n", i); } } }
メソッドとの違いは以下の通りです。
上述の例の通り、 イテレータブロック中で、yield return 文が呼ばれるたびに、 foreach 文中で使われる値を1つ得ます。 for 文や while 文を使わず、ベタに yield return を並べても OK です。
static public IEnumerable GetEnumerable(int from, int to) { yield return 1; yield return 3.14; yield return "文字列"; yield return new System.Drawing.Point(1, 2); yield return 1.0f; }
また、yield break を記述した行まで処理が進むと、イテレータの処理をそこで終了します。
イテレータブロックは静的(static)なものでもインスタンス(非 static)でも、 どちらでも定義できます。 また、プロパティ風の記述も可能です。 上述の例は static なメソッドですが、以下のような非 static なプロパティ風の定義も可能です。
class FromTo { int from, to; public FromTo(int from, int to){this.from = from; this.to = to;} public IEnumerable<int> Enumerable { get { while(from <= to) yield return from++; } } } static void Main(string[] args) { foreach(int i in new FromTo(10, 20).Enumerable) { Console.Write("{0}\n", i); } } }
「コレクションクラスの自作」 で説明したように、 通常、foreach 文で利用できるコレクションクラスを自作するには、 IEnumerable インターフェースを継承し、 GetEnumerator メソッドをオーバーライドします。
C# 2.0 ではこのような方法の他に、 GetEnumerator と言う名前のイテレータブロックを定義することでも コレクションクラスを作成できます。 ここでは、 「ジェネリクス」 で例に挙げた Stack クラスにイテレータを追加してみましょう。
class Stack<Type> { Type[] buf; int top; public Stack(int max) { this.buf = new Type[max]; this.top = 0; } public void Push(Type item) { this.buf[this.top++] = item; } public Type Pop() { return this.buf[--this.top]; } public IEnumerator<Type> GetEnumerator() { for (int i = this.top - 1; i >= 0; --i) yield return buf[i]; } }
「foreach」 で挙げた例を、 ジェネリックスとイテレータを用いて書き直してみます。
using System; using System.Collections.Generic; /// <summary> /// 片方向連結リストクラス /// </summary> class LinearList<T> { /// <summary> /// 連結リストのセル /// </summary> private class Cell { public T value; public Cell next; public Cell(T value, Cell next) { this.value = value; this.next = next; } } private Cell head; public LinearList() { this.head = null; } /// <summary> /// リストに新しい要素を追加 /// </summary> public void Add(T value) { this.head = new Cell(value, head); } /// <summary> /// 列挙子を取得 /// </summary> public IEnumerator<T> GetEnumerator() { for(Cell c = this.head; c != null; c = c.next) { yield return c.value; } } } class ForeachSample { static void Main() { LinearList<int> list = new LinearList<int>(); for(int i=0; i<10; ++i) { list.Add(i * (i + 1) / 2); } foreach(int s in list) { Console.Write(s + " "); } } }
45 36 28 21 15 10 6 3 1 0
イテレータは、 コレクションクラスを実装する際の手間が大幅に削減できる、 非常に便利な機能です。 ですが、少々抽象度が高く、イテレータブロックのコンパイル結果がどうなるのか、 ちょっと想像しづらいと思います。
中には、 中身の分からないものを使うのが怖いという方もいらっしゃるでしょうし、 怖いとまでは言わないものの、少しでもプログラムの効率をよくするために、 コンパイル結果がどうなるかを知りたいと言う方は多いと思います。 なので、イテレータブロックのコンパイル結果について少し触れておきます。 (ちなみに、C# 2.0 の仕様書中にも、このコンパイル結果に関する記事があります。)
イテレータのコンパイル結果ですが、コンパイラが頑張ってくれていて、 結構凄いことをしています。 例えば、先ほど例に挙げた Stack ですが、 以下のようなコードと等価になるそうです。
using System; using System.Collections.Generic; using System.Collections; class Stack<T> : IEnumerable<T> { T[] buf; int top; public Stack(int max) { this.buf = new T[max]; this.top = 0; } public void Push(T item) { this.buf[this.top++] = item; } public T Pop() { return this.buf[--this.top]; } public IEnumerator<T> GetEnumerator() { return new __Enumerator1(this); } class __Enumerator1: IEnumerator<T>, IEnumerator { int __state; T __current; Stack<T> __this; int i; public __Enumerator1(Stack<T> __this) { this.__this = __this; } public T Current { get { return __current; } } object IEnumerator.Current { get { return __current; } } public bool MoveNext() { switch (__state) { case 1: goto __state1; case 2: goto __state2; } i = __this.top - 1; __loop: if (i < 0) goto __state2; __current = __this.buf[i]; __state = 1; return true; __state1: --i; goto __loop; __state2: __state = 2; return false; } public void Dispose() { __state = 2; } void IEnumerator.Reset() { throw new NotSupportedException(); } } }
C# 2.0 コンパイラは、 イテレータブロック内の for 文を、 この MoveNext メソッド内のようなコードに展開してくれるそうです。
また、このコードを見ての通り、 イテレータブロックによって得た IEnumerator は、 実は Reset メソッドをサポートしていません。 Reset を呼ぼうとすると NotSupportedException がスローされます。
「リソースの破棄」 で説明したように、 ファイルなどの、.NET Framework のガベッジコレクションの管理対象外のリソースは明示的な破棄が必要です。
リソースの破棄は、Dispose() メソッドなどを直接呼び出すことでもできますが、 以下のように、イテレータブロック中で Dispose() を呼び出しても、 正しく呼び出されない場合があります。
static IEnumerable<string> Lines(string path) { System.IO.StreamReader sr = new System.IO.StreamReader(path); string line; while ((line = sr.ReadLine()) != null) { yield return line; } sr.Dispose(); // この行は呼ばれない }
イテレータブロックに対しては、 前節で例示したようなコードが自動生成されるわけですが、 その際、 yield return と関係のない場所は無視されます。 上述の例の場合、while ループの外には yield return がないため、 while の後ろの sr.Dispose(); は実行されません。
正しく sr.Dispose(); が呼ばれるようにしたければ、 try-catch-finally 文やusing 文を使います。
static IEnumerable<string> Lines(string path) { using (System.IO.StreamReader sr = new System.IO.StreamReader(path)) { string line; while ((line = sr.ReadLine()) != null) { yield return line; } } }