C#やVB.NETでは、コレクションの各要素に対する繰り返しを行う構文として、foreach文/For Eachステートメントが用意されています。 また、.NET FrameworkにはIEnumerableとIEnumeratorという列挙処理をサポートするためのインターフェイスが用意されています。

IEnumerableとはオブジェクトが列挙(foreach)可能であることを表すインターフェイス、IEnumeratorはオブジェクト内の要素を列挙(foreach)する機能を提供するインターフェイスです。 ここでは、列挙の構文とインターフェイスとの関わりやその動作、また各インターフェイスの実装方法などについて解説します。

§1 列挙操作と列挙子

C#やVB.NETでは、繰り返し構文としてよく似た二つの構文が用意されています。 それは単純な繰り返しのためのfor文/Forステートメントと、コレクションの列挙ためのforeach文/For Eachステートメントです。 For EachステートメントはVBのころから導入されていましたが、VB.NETでも引き続きサポートされC#でもforeach文として導入されています。

ここでは、foreach文による列挙操作と列挙子(IEnumerable, IEnumerator)の関係について見ていきます。

§1.1 for文とforeach文

まずは、for文とforeach文で何が異なるのか、二つの繰り返し構文の違いを見ておきます。 for文では条件式が偽(false)になるまで繰り返し変数をインクリメント(またはデクリメント)し、foreach文ではin以降で指定されるコレクションや配列の中にあるすべての要素の列挙を行います。 以下の例では、int型の配列を用いてfor文とforeach文で列挙を行っています。

using System;

class Sample {
  static void Main()
  {
    int[] arr = new int[] {0, 1, 2, 3, 4};

    // for文で配列の要素を表示する
    Console.WriteLine("for");

    for (int i = 0; i < arr.Length; i++) {
      Console.WriteLine(arr[i]);
    }

    // foreach文で配列の要素を表示する
    Console.WriteLine("foreach");

    foreach (int e in arr) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
for
0
1
2
3
4
foreach
0
1
2
3
4

この例において、for文とforeach文での違いは、for文では変数iを使って配列のインデックスを指定することで列挙しているのに対し、foreach文では指定した変数eに配列の各要素を格納させることで列挙しているという点です。

もう少し違いを見るために、配列をQueueクラスに置き換えてみます。 Queueクラスはインデクサをサポートしていないので、for文を用いてインデックスを指定した列挙は出来ません。 そのため、以下のコードにおいて、foreach文による列挙の部分は問題なく動作しますが、for文による列挙の部分でコンパイルエラーとなります。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Queue<int> queue = new Queue<int>(new int[] {0, 1, 2, 3, 4});

    // for文でQueueに格納されている要素を表示する
    Console.WriteLine("for");

    for (int i = 0; i < queue.Count; i++) {
      // error CS0021: 角かっこ [] 付きインデックスを'System.Collections.Generic.Queue<int>'型の式に適用することはできません。
      Console.WriteLine(queue[i]);
    }

    // foreach文でQueueに格納されている要素を表示する
    Console.WriteLine("foreach");

    foreach (int e in queue) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

このように、for文ではインデックスが指定できないと列挙出来ませんが、foreach文ではインデックスが指定出来なくても列挙が出来ます。 ではforeach文で列挙できない場合とはどのような場合なのか、その違いを明らかにするために、別の例を挙げてみます。

次のコードでは、インデクサをサポートする配列の簡易ラッパークラスを作り、for文とforeach文を用いた列挙操作を記述しています。

using System;

// インデクサをサポートする配列のラッパークラス
class IntArray {
  private int[] arr;

  public IntArray(int[] arr)
  {
    this.arr = arr;
  }

  // インデクサ
  public int this[int index] {
    get { return arr[index]; }
    set { arr[index] = value; }
  }

  // 長さ
  public int Length {
    get { return arr.Length; }
  }
}

class Sample {
  static void Main()
  {
    IntArray arr = new IntArray(new int[] {0, 1, 2, 3, 4});

    // for文で格納されている要素を表示する
    Console.WriteLine("for");

    for (int i = 0; i < arr.Length; i++) {
      Console.WriteLine(arr[i]);
    }

    // foreach文で格納されている要素を表示する
    Console.WriteLine("foreach");

    // error CS1579: foreach ステートメントは、'IntArray' が 'GetEnumerator' のパブリック定義を含んでいないため、型 'IntArray' の変数に対して使用できません。
    foreach (int e in arr) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

このコードはfor文での列挙は問題なく出来ますが、コメントにあるエラーメッセージの通りforeach文での列挙はコンパイルエラーとなります。

ここまでの例を見て分かる通り、foreach文で列挙できるかどうかは、配列やコレクションのようにインデクサをサポートしているかどうかとは別の理由によるものであるということが分かると思います。 結論から言うと、foreach文で列挙できるかどうかは、列挙しようとするオブジェクトがIEnumerableインターフェイスを実装しているかどうかで決まります。

§1.2 foreach文とIEnumerable, IEnumeratorの関係

foreach文/For Eachステートメントでの列挙操作を出来るようにするためには、列挙される型がIEnumerableインターフェイスを実装していなければなりません。 IEnumerableインターフェイスは、型が列挙可能(enumerable)であることを表すためのインターフェイスです。 型がこのインターフェイスを実装している場合、foreach文での列挙が出来るようになります。 配列やコレクションクラスがforeach文で列挙できるのは、配列の実体であるArrayクラスや個々のコレクションクラスがIEnumerableインターフェイスを実装しているためです。

IEnumerableインターフェイスには唯一のメソッドGetEnumeratorが存在します。 このメソッドは、列挙操作を行うための列挙子(enumerator)であるIEnumeratorインターフェイスを取得するためのメソッドです。

この二つのインターフェイスとforeach文の関係と動作を整理すると、次のようになります。

  1. foreach文での列挙を行う際に、型が列挙可能(enumerable)かどうか調べる (IEnumerableインターフェイスを実装しているか調べる)
  2. 列挙可能なら、列挙子(enumerator)を取得する (IEnumerable.GetEnumeratorメソッドでIEnumeratorを取得する)
  3. 取得した列挙子を使って列挙操作を行う

個々の詳細は後述するとして、先の例をIEnumerableインターフェイスを実装したものに書き換えてみます。

using System;
using System.Collections;

// インデクサをサポートする配列のラッパークラス
class IntArray : IEnumerable {
  private int[] arr;

  public IntArray(int[] arr)
  {
    this.arr = arr;
  }

  // インデクサ
  public int this[int index] {
    get { return arr[index]; }
    set { arr[index] = value; }
  }

  // 長さ
  public int Length {
    get { return arr.Length; }
  }

  // IEnumerable.GetEnumeratorの実装
  public IEnumerator GetEnumerator()
  {
    // Array.GetEnumerator()の戻り値を流用する
    return arr.GetEnumerator();
  }
}

class Sample {
  static void Main()
  {
    IntArray arr = new IntArray(new int[] {0, 1, 2, 3, 4});

    // for文で格納されている要素を表示する
    Console.WriteLine("for");

    for (int i = 0; i < arr.Length; i++) {
      Console.WriteLine(arr[i]);
    }

    // foreach文で格納されている要素を表示する
    Console.WriteLine("foreach");

    foreach (int e in arr) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
for
0
1
2
3
4
foreach
0
1
2
3
4

このように、IEnumerableインターフェイスを実装することで、foreach文による列挙が行えるようになります。

§2 IEnumerableとIEnumerator

型が列挙可能となるためには、IEnumerableインターフェイスを実装し、IEnumeratorインターフェイスを返す必要があることが分かりました。 ここでは、IEnumerableとIEnumeratorを実装する方法と、これらのインターフェイスの動作について見ていきます。 また、これらのインターフェイスのジェネリック版であるIEnumerable<T>インターフェイスとIEnumerator<T>インターフェイスについても見ていきます。

§2.1 IEnumerableとIEnumerator

IEnumerableとIEnumeratorを実装するために、それぞれのインターフェイスの仕様について見ていきます。 まず、型を列挙可能にするためのインターフェイスIEnumerableは、次の仕様を満たすように実装する必要があります。

IEnumerableインターフェイス
以下の1つのメンバを実装する必要がある。
GetEnumeratorメソッド
インターフェイスを実装する型の要素を列挙するための列挙子IEnumeratorを返す。

続いて要素を列挙するためのインターフェイスIEnumeratorは、次の仕様を満たすように実装する必要があります。

IEnumeratorインターフェイス
以下の3つのメンバを実装する必要がある。
Currentプロパティ
列挙子が指し示すコレクション内の要素をObject型で返す。 読み取り専用プロパティ。
MoveNextメソッド
列挙子が指し示す要素の位置を次に進める。 位置がコレクションの末尾を超えた場合(=これ以上列挙できる要素が無い場合)はfalse、それ以外の場合はtrueを返す。
Resetメソッド
列挙子が指し示す要素の位置を最初の位置に戻し、列挙子の状態を初期状態に戻す。 (必ずしも実装する必要はなく、NotSupportedExceptionをスローしてもよい)

IEnumerator.ResetメソッドはCOM相互運用で使用されるメソッドで、foreach文などによる列挙操作では呼び出されません。 そのため、このメソッドでは初期状態に戻すように実装する必要はなく、例外NotSupportedExceptionをスローするようにしてもよいとされています。 また、列挙操作中にコレクションが変更(追加・削除・挿入など)され、列挙操作を継続できない場合は、InvalidOperationExceptionをスローするようにします。

早速、上記の仕様を満たすクラスを用意して、列挙操作を行ってみます。 以下の例は、列挙可能なコレクションに見立てたクラスEnumerableと、コレクションの列挙子に見立てたクラスEnumeratorを作成したものです。 この例では、Enumerableクラスはコレクションとしての実体は持っていませんが、Enumerableクラスで作成されるEnumeratorクラスは{0, 1, 2}の3つの要素を列挙します。

using System;
using System.Collections;

class Enumerable : IEnumerable {
  // IEnumerable.GetEnumeratorの実装
  public IEnumerator GetEnumerator()
  {
    // IEnumerableを実装するクラスEnumeratorのインスタンスを返す
    return new Enumerator();
  }
}

class Enumerator : IEnumerator {
  // 現在の位置を表す変数
  int pos = -1;

  // IEnumerator.MoveNextの実装
  public bool MoveNext()
  {
    // 現在の位置を1増やす
    pos++;

    if (pos == 3) {
      // 位置が3になったら、それ以上列挙できる要素は無いものとしてfalseを返す
      return false;
    }
    else {
      // それ以外の場合は、まだ列挙できる要素があるものとしてtrueを返す
      return true;
    }
  }

  // IEnumerator.Currentの実装
  public object Current
  {
    // 現在の位置自体を要素の値として返す
    get { return pos; }
  }

  // IEnumerator.Resetの実装
  public void Reset()
  {
    // 実装は省略、NotSupportedExceptionをスローするようにする
    throw new NotSupportedException();
  }
}

class Sample {
  static void Main()
  {
    Enumerable enumerable = new Enumerable();

    foreach (int e in enumerable) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
0
1
2

IEnumerableとIEnumeratorの仕様と照らし合わせながら上記の実装と実行結果を見ていけば、各インターフェイスの役割と動作について理解できると思います。 列挙時の動作について整理すると、次のようになります。

  1. (foreach文/For Eachステートメントなどで)IEnumerable.GetEnumeratorメソッドが呼ばれ、IEnumeratorが取得される。
  2. 取得されたIEnumeratorのMoveNextメソッドが呼ばれ、要素の位置が次の位置に進められる。 このときの戻り値が、
    1. falseだった場合はそれ以上列挙できる要素がないため、列挙が終了する。
    2. trueだった場合はまだ列挙できる要素があるため、列挙が継続する。
  3. IEnumerator.Currentプロパティが参照され、現在の位置にある要素が取得される。
  4. 2に戻り、列挙を繰り返す。

IEnumerableとIEnumeratorの動作と列挙操作について理解を深めるために、違った例を挙げてみます。 次の例は、for文/foreach文を使わずに配列を列挙する例です。 配列を一旦IEnumerableにキャストし、GetEnumeratorでIEnumeratorを取得することで配列内の要素を列挙しています。

using System;
using System.Collections;

class Sample {
  static void Main()
  {
    int[] arr = new int[] {0, 1, 2};

    // IEnumerableにキャスト
    IEnumerable enumerable = arr;

    // IEnumeratorを取得
    IEnumerator enumerator = enumerable.GetEnumerator();
    int e;

    // MoveNextがfalseを返すまで繰り返す
    while (enumerator.MoveNext()) {
      // 現在の値を取得
      e = (int)enumerator.Current;

      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
0
1
2

最後に、もう少し具体的な実装の例を挙げます。 以下の例では、指定された長さの配列をラップするクラスIntArrayと、その列挙子IntArrayEnumeratorを作成しています。 IntArrayEnumeratorはIntArray以外のクラスから使われることは無いので、IntArrayの入れ子クラスとして実装しています。

using System;
using System.Collections;

class IntArray : IEnumerable {
  private int[] arr;

  public IntArray(int length)
  {
    arr = new int[length];
  }

  public int this[int index] {
    get { return arr[index]; }
    set { arr[index] = value; }
  }

  public int Length {
    get { return arr.Length; }
  }

  public IEnumerator GetEnumerator()
  {
    // 現在のインスタンスを渡して列挙子を作成
    return new IntArrayEnumerator(this);
  }

  // IntArray用の列挙子
  private class IntArrayEnumerator : IEnumerator {
    private IntArray arr; // この列挙子が列挙するIntArrayのインスタンス
    private int index = -1; // 列挙子の現在の位置

    public IntArrayEnumerator(IntArray arr)
    {
      this.arr = arr;
    }

    public bool MoveNext()
    {
      index++;

      if (index == arr.Length) {
        return false;
      }
      else {
        return true;
      }
    }

    public object Current {
      get { return arr[index]; }
    }

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

class Sample {
  static void Main()
  {
    IntArray arr = new IntArray(10); // 要素数10で初期化

    // 値を設定
    for (int i = 0; i < arr.Length; i++) {
      arr[i] = i;
    }

    // 要素を列挙
    foreach (int e in arr) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
0
1
2
3
4
5
6
7
8
9

§2.2 IEnumerable<T>とIEnumerator<T>

IEnumerableとIEnumeratorにはそのジェネリック版となるインターフェイスIEnumerable<T>IEnumerator<T>が存在します。 先に実装例を見ることで、相違点を見ていきます。 以下の例は、先に挙げたIEnumerable・IEnumeratorでの実装例をIEnumerable<T>・IEnumerator<T>を使って書き換えたものです。

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

class Enumerable : IEnumerable<int> {
  // IEnumerable<int>.GetEnumeratorの実装
  public IEnumerator<int> GetEnumerator()
  {
    return new Enumerator();
  }

  // IEnumerable.GetEnumeratorの実装
  IEnumerator IEnumerable.GetEnumerator()
  {
    // IEnumerable<int>.GetEnumeratorの実装を使う
    return GetEnumerator();
  }
}

class Enumerator : IEnumerator<int> {
  int pos = -1;

  // IEnumerator<int>.MoveNextの実装
  public bool MoveNext()
  {
    pos++;

    if (pos == 3) {
      return false;
    }
    else {
      return true;
    }
  }

  // IEnumerator<int>.Currentの実装
  public int Current {
    get { return pos; }
  }

  // IEnumerator.Currentの実装
  object IEnumerator.Current
  {
    // IEnumerator<int>.Currentの値を返す
    get { return Current; }
  }

  // IEnumerator<int>.Resetの実装
  public void Reset()
  {
    throw new NotSupportedException();
  }

  // IEnumerator<int>.Disposeの実装
  public void Dispose()
  {
    Console.WriteLine("disposed");
  }
}

class Sample {
  static void Main()
  {
    Enumerable enumerable = new Enumerable();

    foreach (int e in enumerable) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
0
1
2
disposed

IEnumerable<T>とIEnumerator<T>について、それぞれ特筆すべき点をまとめます。

IEnumerable<T>インターフェイス
IEnumerableインターフェイスを継承しているため、実装の際は以下のメンバに加え、IEnumerableのメンバも実装する必要がある。
IEnumerable<T>.GetEnumeratorメソッド
ジェネリックな列挙子であるIEnumerator<T>を返す必要がある点以外はIEnumerable.GetEnumeratorメソッドと同じ。
IEnumerator<T>インターフェイス
IEnumeratorインターフェイスおよびIDisposableインターフェイスを継承しているため、実装の際は以下のメンバに加え、IEnumeratorのメンバも実装する必要がある。
IEnumerator<T>.Currentプロパティ
現在の要素を、厳密に型定義された値として返すという点以外は、object型であるIEnumerator.Currentプロパティと同じ。
IEnumerator<T>.Disposeメソッド
IDisposableから継承されるメソッド。 列挙子が解放すべきリソースを保持する場合は、このメソッドで解放処理を実装する必要がある。 foreach文では、列挙操作が終了(または中断)する際に自動的に呼び出される。

IEnumerable<T>とIEnumerator<T>では、ジェネリック版に固有のメンバとIEnumerable・IEnumeratorから継承されるメンバの二種類を実装する必要がありますが、IEnumerable<T>・IEnumerator<T>の実装を流用することが出来るので、ほとんどの場合それぞれのメンバで別々の実装を二つ用意する必要はありません。 上記の例でも、非ジェネリックなメンバの実装に、ジェネリックなメンバの実装を流用しています。

また、IEnumerable<T>・IEnumerator<T>とIEnumerable・IEnumeratorで同じ名前のメンバを実装しなければなりませんが、C#では明示的なインターフェイスの実装、VBではプライベートな別名のメンバによる実装を用いることで、それぞれのインターフェイスのメンバを実装することが出来ます。 なお、IEnumerable<T>・IEnumerator<T>を実装している型の場合でも、列挙時の動作はDisposeメソッドが呼ばれる以外はIEnumerable・IEnumeratorの場合と同じです。

次の例は、for文/foreach文を使わずに配列を列挙する例です。 配列を一旦IEnumerable<T>にキャストして、GetEnumeratorでIEnumerator<T>を取得することで配列内の要素を列挙しています。 また、usingステートメントを使うことで、取得したIEnumerator<T>のDisposeメソッドを確実に呼び出すようにしています。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    int[] arr = new int[] {0, 1, 2};

    // IEnumerable<int>にキャスト
    IEnumerable<int> enumerable = arr;

    // IEnumerator<int>を取得
    using (IEnumerator<int> enumerator = enumerable.GetEnumerator()) {
      int e;

      // MoveNextがfalseを返すまで繰り返す
      while (enumerator.MoveNext()) {
        // 現在の値を取得
        e = (int)enumerator.Current;

        Console.WriteLine(e);
      }
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
0
1
2

§3 IEnumerable・IEnumeratorを実装しない型の列挙

ここまでの解説では、列挙を行うためにはIEnumerableを実装する必要があるとしてきました。 しかし実際には、C#のforeach文、VBのFor EachステートメントではIEnumerableを実装していない型でも列挙操作を行うことが出来ます。 以下は、IEnumerable・IEnumeratorを実装していないクラスで列挙操作を行う例です。

using System;

class Enumerable /* : IEnumerable */ {
  public Enumerator GetEnumerator()
  {
    return new Enumerator();
  }
}

class Enumerator /* : IEnumerator */ {
  int pos = -1;

  public bool MoveNext()
  {
    pos++;

    if (pos == 3) {
      return false;
    }
    else {
      return true;
    }
  }

  public int Current {
    get { return pos; }
  }

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

class Sample {
  static void Main()
  {
    Enumerable enumerable = new Enumerable();

    foreach (int e in enumerable) {
      Console.WriteLine(e);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
0
1
2

このように、IEnumerable・IEnumeratorを実装していなくても、IEnumerable・IEnumeratorと同等のメンバを持つ型であればforeach文/For Eachステートメントで列挙を行うことは出来ます。 これらはいずれもコンパイラによりサポートされる機能です。 ただ、このようにして実装された型を他の言語で使用した場合でも列挙出来るとは限らないため、他の言語との相互運用性を考慮するとIEnumerable・IEnumeratorを用いて実装すべきです。