C#やVB.NETでは、コレクションの各要素に対する繰り返しを行う構文として、foreach文/For Eachステートメントが用意されています。 また、.NET FrameworkにはIEnumerableとIEnumeratorという列挙処理をサポートするためのインターフェイスが用意されています。
IEnumerableとはオブジェクトが列挙(foreach)可能であることを表すインターフェイス、IEnumeratorはオブジェクト内の要素を列挙(foreach)する機能を提供するインターフェイスです。 ここでは、列挙の構文とインターフェイスとの関わりやその動作、また各インターフェイスの実装方法などについて解説します。
列挙操作と列挙子
C#やVB.NETでは、繰り返し構文としてよく似た二つの構文が用意されています。 それは単純な繰り返しのためのfor文/Forステートメントと、コレクションの列挙ためのforeach文/For Eachステートメントです。 For EachステートメントはVBのころから導入されていましたが、VB.NETでも引き続きサポートされC#でもforeach文として導入されています。
ここでは、foreach文による列挙操作と列挙子(IEnumerable, IEnumerator)の関係について見ていきます。
for文とforeach文
まずは、for文とforeach文で何が異なるのか、二つの繰り返し構文の違いを見ておきます。 for文では条件式が偽(false)になるまで繰り返し変数をインクリメント(またはデクリメント)し、foreach文ではin以降で指定されるコレクションや配列の中にあるすべての要素の列挙を行います。 以下の例では、int型の配列を用いてfor文とforeach文で列挙を行っています。
この例において、for文とforeach文での違いは、for文では変数iを使って配列のインデックスを指定することで列挙しているのに対し、foreach文では指定した変数eに配列の各要素を格納させることで列挙しているという点です。
もう少し違いを見るために、配列をQueueクラスに置き換えてみます。 Queueクラスはインデクサをサポートしていないので、for文を用いてインデックスを指定した列挙は出来ません。 そのため、以下のコードにおいて、foreach文による列挙の部分は問題なく動作しますが、for文による列挙の部分でコンパイルエラーとなります。
このように、for文ではインデックスが指定できないと列挙出来ませんが、foreach文ではインデックスが指定出来なくても列挙が出来ます。 ではforeach文で列挙できない場合とはどのような場合なのか、その違いを明らかにするために、別の例を挙げてみます。
次のコードでは、インデクサをサポートする配列の簡易ラッパークラスを作り、for文とforeach文を用いた列挙操作を記述しています。
このコードはfor文での列挙は問題なく出来ますが、コメントにあるエラーメッセージの通りforeach文での列挙はコンパイルエラーとなります。
ここまでの例を見て分かる通り、foreach文で列挙できるかどうかは、配列やコレクションのようにインデクサをサポートしているかどうかとは別の理由によるものであるということが分かると思います。 結論から言うと、foreach文で列挙できるかどうかは、列挙しようとするオブジェクトがIEnumerableインターフェイスを実装しているかどうかで決まります。
foreach文とIEnumerable, IEnumeratorの関係
foreach文/For Eachステートメントでの列挙操作を出来るようにするためには、列挙される型がIEnumerableインターフェイスを実装していなければなりません。 IEnumerableインターフェイスは、型が列挙可能(enumerable)であることを表すためのインターフェイスです。 型がこのインターフェイスを実装している場合、foreach文での列挙が出来るようになります。 配列やコレクションクラスがforeach文で列挙できるのは、配列の実体であるArrayクラスや個々のコレクションクラスがIEnumerableインターフェイスを実装しているためです。
IEnumerableインターフェイスには唯一のメソッドGetEnumeratorが存在します。 このメソッドは、列挙操作を行うための列挙子(enumerator)であるIEnumeratorインターフェイスを取得するためのメソッドです。
この二つのインターフェイスとforeach文の関係と動作を整理すると、次のようになります。
- foreach文での列挙を行う際に、型が列挙可能(enumerable)かどうか調べる (IEnumerableインターフェイスを実装しているか調べる)
- 列挙可能なら、列挙子(enumerator)を取得する (IEnumerable.GetEnumeratorメソッドでIEnumeratorを取得する)
- 取得した列挙子を使って列挙操作を行う
個々の詳細は後述するとして、先の例をIEnumerableインターフェイスを実装したものに書き換えてみます。
このように、IEnumerableインターフェイスを実装することで、foreach文による列挙が行えるようになります。
IEnumerableとIEnumerator
型が列挙可能となるためには、IEnumerableインターフェイスを実装し、IEnumeratorインターフェイスを返す必要があることが分かりました。 ここでは、IEnumerableとIEnumeratorを実装する方法と、これらのインターフェイスの動作について見ていきます。 また、これらのインターフェイスのジェネリック版であるIEnumerable<T>インターフェイスとIEnumerator<T>インターフェイスについても見ていきます。
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つの要素を列挙します。
IEnumerableとIEnumeratorの仕様と照らし合わせながら上記の実装と実行結果を見ていけば、各インターフェイスの役割と動作について理解できると思います。 列挙時の動作について整理すると、次のようになります。
- (foreach文/For Eachステートメントなどで)IEnumerable.GetEnumeratorメソッドが呼ばれ、IEnumeratorが取得される。
- 取得されたIEnumeratorのMoveNextメソッドが呼ばれ、要素の位置が次の位置に進められる。 このときの戻り値が、
- falseだった場合はそれ以上列挙できる要素がないため、列挙が終了する。
- trueだった場合はまだ列挙できる要素があるため、列挙が継続する。
- IEnumerator.Currentプロパティが参照され、現在の位置にある要素が取得される。
- 2に戻り、列挙を繰り返す。
IEnumerableとIEnumeratorの動作と列挙操作について理解を深めるために、違った例を挙げてみます。 次の例は、for文/foreach文を使わずに配列を列挙する例です。 配列を一旦IEnumerableにキャストし、GetEnumeratorでIEnumeratorを取得することで配列内の要素を列挙しています。
最後に、もう少し具体的な実装の例を挙げます。 以下の例では、指定された長さの配列をラップするクラスIntArrayと、その列挙子IntArrayEnumeratorを作成しています。 IntArrayEnumeratorはIntArray以外のクラスから使われることは無いので、IntArrayの入れ子クラスとして実装しています。
IEnumerable<T>とIEnumerator<T>
IEnumerableとIEnumeratorにはそのジェネリック版となるインターフェイスIEnumerable<T>とIEnumerator<T>が存在します。 先に実装例を見ることで、相違点を見ていきます。 以下の例は、先に挙げたIEnumerable・IEnumeratorでの実装例をIEnumerable<T>・IEnumerator<T>を使って書き換えたものです。
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メソッドを確実に呼び出すようにしています。
IEnumerable・IEnumeratorを実装しない型の列挙
ここまでの解説では、列挙を行うためにはIEnumerableを実装する必要があるとしてきました。 しかし実際には、C#のforeach文、VBのFor EachステートメントではIEnumerableを実装していない型でも列挙操作を行うことが出来ます。 以下は、IEnumerable・IEnumeratorを実装していないクラスで列挙操作を行う例です。
このように、IEnumerable・IEnumeratorを実装していなくても、IEnumerable・IEnumeratorと同等のメンバを持つ型であればforeach文/For Eachステートメントで列挙を行うことは出来ます。 これらはいずれもコンパイラによりサポートされる機能です。 ただ、このようにして実装された型を他の言語で使用した場合でも列挙出来るとは限らないため、他の言語との相互運用性を考慮するとIEnumerable・IEnumeratorを用いて実装すべきです。