IEnumerableとIEnumerator
《 初回公開:2020/06/28 , 最終更新:未 》
visual studio2019 C#8.0および一部C#7.3で動作確認。
【 目次 】
IEnumerableインターフェイスを実装したクラスは、foreachステートメントを使って内部に保持している値を列挙する事ができる。
IEnumerableインターフェイスとIEnumeratorインターフェイス
IEnumerableインターフェイスの定義は
[NullableContextAttribute(1)] public interface IEnumerable { IEnumerator GetEnumerator(); }
見ての通りIEnumerableインターフェイスを実装するクラスには、列挙される値を取得するためIEnumeratorインターフェイスを返すGetEnumeratorメソッドの実装が必要。
IEnumeratorインターフェイスの定義は
[NullableContextAttribute(2)] public interface IEnumerator { object? Current { get; } bool MoveNext(); void Reset(); }
IEnumeratorとIEnumerableを実装したクラスの例を示す。
ReverceEnumerableクラスはコンストラクタとして文字列を受け取る。
そしてforeachループでコンストラクタで受け取った文字列を、最後の文字から逆順に一文字づつchar型の値として返す。
あまり実用的な例では無いが。
class ReverceEnumerator : IEnumerator { private int index; string str; public object Current => str[str.Length - index - 1]; public bool MoveNext() { if (index >= str.Length - 1) return false; index++; return true; } public void Reset() => index = -1; public ReverceEnumerator(string str) { this.str = str; Reset(); } } class ReverceEnumerable : IEnumerable { string str; public IEnumerator GetEnumerator() { return new ReverceEnumerator(str); } public ReverceEnumerable(string str) { this.str = str; } }
上記のReverceEnumerableオブジェクトの要素をforeachループを使って取り出すと
ReverceEnumerable rse = new ReverceEnumerable("あいうえお"); foreach (char c in rse) { Console.Write(c); } Console.WriteLine();
実行結果
おえういあ
ReverceEnumerableクラスのGetEnumeratorメソッドは、IEnumeratorインターフェイスを実装するReverceEnumeratorオブジェクトを生成して、そのオブジェクトを返す。
ReverceEnumeratorクラスは、IEnumeratorインターフェイスの実装に必要なReset,MoveNextメソッドとCurrentプロパティを備えていて、Resetメソッドはforeachループの開始時に最初に一回だけ呼び出されてEnumeratorの初期化をおこなう。
Enumeratorの初期化というのは、Enumeratorは内部にカウンターを持っていてそのカウンターの値を次にMoveNextメソッドが呼ばれたときに最初の値を示すように、最初の値のひとつ前の位置に設定する。
その後、foreachループはMoveNextメソッドを呼び出し、列挙値を取得する前にカウンタを一つ進める。
そしてCurrentプロパティを呼び出し各列挙値を取得する。
二回目からはResetメソッドは呼び出されず、MoveNextメソッド,Currentプロパティの順に呼び出される。
またMoveNextメソッドはカウンターが最後の位置を指している場合(次に列挙する値がもう無い場合)に、それをforeachループに知らせるためにfalseを返す。
そして、それ以外の場合をループを続けるようにtrueを返す。
このようにしてforeachループとIEnumerableインターフェイスとが連動する事により、IEnumeratorクラスは内部に含む要素を最初から最後まで列挙する事ができる。
この一連のシーケンスは、VisualStudioでReset,MoveNextメソッドとCurrentプロパティにブレークポイントを設定してデバッガを動作させる事で確認できる。
foreachループを使わずに、この動作をIEnumeratorインターフェイスのメソッドやプロパティを直接呼び出して同等のコードを記述すると、以下のようなコードになる。
ReverceEnumerable rse = new ReverceEnumerable("あいうえお"); // foreachの部分 - start IEnumerator enumerator = rse.GetEnumerator(); enumerator.Reset(); while(enumerator.MoveNext()) { char c = (char)enumerator.Current; Console.Write(c); } // foreachの部分 - end Console.WriteLine();
ジェネリック版のIEnumerable<T>インターフェイスとIEnumerator<T>インターフェイス
ジェネリック型のIEnumerable<T>インターフェイスも定義されていて
IEnumerable<T>インターフェイスの定義は
public interface IEnumerable<[NullableAttribute(2)] out T> : IEnumerable { [NullableContextAttribute(1)] IEnumerator<T> GetEnumerator(); }
同様にIEnumerator<T>の定義は
public interface IEnumerator<[NullableAttribute(2)] out T> : IEnumerator, IDisposable { [NullableAttribute(1)] T Current { get; } }
ジェネリック版のIEnumerable<T>インターフェイスとIEnumerator<T>インターフェイスは後から追加されたもののようで、これからはこちらの方を使った方が。
前述のReverceEnumeratorクラスの例をジェネリック版を使って実装してみると
class ReverceGenericEnumerator : IEnumerator<char> { private int index; string str; public char Current => str[str.Length - index - 1]; object IEnumerator.Current => Current; public void Dispose() { } public bool MoveNext() { if (index >= str.Length - 1) return false; index++; return true; } public void Reset() => index = -1; public ReverceGenericEnumerator(string str) { this.str = str; Reset(); } } public class ReverceGenericEnumerable : IEnumerable<char> { string str; public IEnumerator<char> GetEnumerator() { return new ReverceGenericEnumerator(str); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public ReverceGenericEnumerable(string str) { this.str = str; } public void Add(string str) { this.str += str; } }
ここではReverceEnumeratorクラスのジェネリック版というだけでは面白くないので、上記のコードではコレクション初期化子の機能も持たせている。
コレクション初期化子を使う場合にもIEnumerableが必要であってサンプルコードとしては少々複雑になってしまってふさわしくないが、これもIEnumerableインターフェースの応用例としてみていただきたい。
コレクション初期化子の機能を持たせるためにはAddメソッドが必要がありこれを追加している。
ReverceGenericEnumerableオブジェクトを生成してforeachループを使うと
ReverceGenericEnumerable rse = new ReverceGenericEnumerable("あいうえお") { "かきくけこ","さしすせそ", }; foreach (char c in rse) { Console.WriteLine(c); } ReverceGenericEnumerable rse = new ReverceGenericEnumerable("あいうえお") { "かきくけこ","さしすせそ", }; foreach (char c in rse) { Console.Write(c); } Console.WriteLine();