++C++; // 未確認飛行 C MVVMパターンを使ったクロス・ターゲット開発 C#たんと学ぶ わりと硬派なソフトウェア開発講座 番外編「Windows Phone 7」

Top総合 目次C# によるプログラミング入門

イテレーター

このエントリーをはてなブックマークに追加

目次

キーワード

概要

C# の foreach 構文は、コレクションクラスの利用者側から見ると非常に便利な機能です。 しかしながら、実装側から見た場合、IEnumerableIEnumeratorインターフェースを実装する必要があり、結構面倒な作業が必要でした。

この実装側の労力を軽減するために、C# 2.0ではイテレーター構文というものが追加されました。

ポイント
  • イテレーター構文: IEnumerator を簡単に実装するための機能。
  • return の代わりに yield return

イテレータブロック

クラス中にイテレーターブロック(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 という単語は「譲る」という意味です (車文化のアメリカでよく使われるのは、「車線を譲る」(他の車を通すために速度を落としたり、脇道に止めたり))。 イテレーター ブロックの場合、"yield control to another method"(制御フローを他の処理に譲る)というような意味合いになります。

メソッドとの違いは以下の通りです。

  • 戻り値の型が以下のうちのいずれか
    • System.Collections.IEnumerator
    • System.Collections.Generic.IEnumerator<T>
    • System.Collections.IEnumerable
    • System.Collections.Generic.IEnumerable<T>
  • return の変わりに yield return というキーワードを使う。
  • break の変わりに yield break というキーワードを使う。

上述の例の通り、 イテレーター ブロック中で、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);
    }
  }
}

GetEnumerator

コレクションクラスの自作」 で説明したように、 通常、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 の仕様書中にも、このコンパイル結果に関する記事があります。)

イテレーターのコンパイル結果ですが、コンパイラが頑張ってくれていて、 結構凄いことをしています。 一種の状態機械(state machine)を自動生成していて、 例えば、先ほど例に挙げた 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 メソッド内のようなコードに展開してくれるそうです。 やっていることを簡単に言うと、yield return x; の部分を以下のように置き換えています。

state = State1; // 次に復帰するときのための状態の記録
Current = x;    // 戻り値を Current に保持
return true;    // いったん処理終了
case State1:    // 次に呼ばれたときに続きから処理するためのラベル

そして、最後に、これを switch 文で囲う(もしくは、state に応じた goto 文を先頭に追加する)ことで、 処理の一時中断と再開を実現します。

ちなみに、このコードを見ての通り、 イテレーター ブロックによって得た 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;
    }
  }
}

[お問い合わせ](q)