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インターフェイスの実装に必要なResetMoveNextメソッドと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&lt;T&gt; 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();

参考記事

ページのトップへ戻る