74
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

[C#]IEnumeratorとIEnumerableを調べた

経緯

UnityでC#を用いた開発などを行っているとよく IEnumeratorIEnumerable というインターフェース1を扱いますが、使い方などをある程度わかっていても、実はちゃんと調べたことないなと思ったので、調べてみました。

IEnumerator とは

IEnumerator は反復処理をサポートするインターフェースです。 System.Collections 名前空間内にあり、以下の関数・プロパティを持っています。

bool MoveNext();
void Reset();
object Current;

MoveNext() でイテレータを次に進ませて、 Current でその値を取得します。 Reset() を呼び出すとイテレータを初期位置に戻します。
MoveNext() の返り値は bool ですが、これはイテレータを進ませることができた場合に true が返ります。以下のコードは渡された IEnumerator の値を1つずつコンソールに表示するサンプルです。

void PrintEnumerator(System.Collections.IEnumerator enumerator)
{
    while(enumerator.MoveNext())
    {
        System.Console.WriteLine(enumerator.Current);
    }
}

IEnumerable とは

IEnumerable は反復処理をサポートする列挙子(つまり IEnumerator )を公開するインターフェースです。こちらも System.Collections 名前空間内にあり、 IEnumerator を返す GetEnumerator 関数を持っています。

IEnumerator GetEnumerator();

IEnumerable インターフェースを実装していると、 foreach を用いて反復処理を行うことができます。2
以下のコードは渡された IEnumerable の値を1つずつコンソールに表示するサンプルです。

void PrintEnumerable(System.Collections.IEnumerable enumerable)
{
    foreach(var value in enumerable)
    {
        System.Console.WriteLine(value);
    }
}

List<T> 型は IEnumerable を実装していますし、配列型は IEnumerable を実装した Array クラスを継承しています。これらを foreach に指定できるのはこのためです。

foreach は糖衣構文

さて、サンプルコードの PrintEnumerator 関数と PrintEnumerable 関数を見比べてみてください。
やっていることはほぼ同じですが、 PrintEnumrable のほうが中身がスッキリしていますね。
PrintEnumerator は毎ループごとに MoveNext() を実行して返り値を見て、 true であれば値があるということで Current を参照…という、ちょっと面倒なコードになっています。

foreach はこれを簡潔に書けるように GetEnumerator() を呼び出し、取得したクラスの MoveNext() Current を用いたループに展開されます。例えば PrintEnumerable はコンパイル時に以下のようなコードに展開されます。

void PrintEnumerable(System.Collections.IEnumerable enumerable)
{
    System.Collections.IEnumerator e = enumerable.GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            object v = e.Current;
            System.WriteLine(v);
        }
    }
    finally
    {
        System.IDisposable d = e as System.IDisposable;
        if (d != null) d.Dispose();
    }
}

IEnumerator はコルーチンのためだけのものではない

UnityからC#を触り始めた人は勘違いしやすいのですが、 IEnumerator はUnityのコルーチンのために存在しているインターフェースではありません。
ここまでの説明の通り、 IEnumerator は反復処理をサポートするインターフェースです。Unityがコルーチンの機能を提供するために IEnumerator を利用しているというだけです。

実際、以下のコードはUnityで実行可能で、この場合 Start が実行されたフレームですべての処理が終了します。3

using System.Collections;
using UnityEngine;

public class IEnumeratorTest : MonoBehaviour
{
    void Start()
    {
        var enumerator = GetEnumerator();
        Debug.Log("Loop Start");
        while (enumerator.MoveNext())
        {
            Debug.Log(enumerator.Current);
        }
    }

    public IEnumerator GetEnumerator()
    {
        Debug.Log("GetEnumerator: null");
        yield return null;
        Debug.Log("GetEnumerator: WaitForEndOfFrame");
        yield return new WaitForEndOfFrame();
        Debug.Log("GetEnumerator: WaitForSeconds(1f)");
        yield return new WaitForSeconds(1f);
    }
}

遅延評価

ところで、上記のサンプルでは最初に「Loop Start」のログが表示されます。
最初に GetEnumerator() を呼んでいるので、「GetEnumerator: null」が先に表示されるように考えてしまいますが、 GetEnumerator() はあくまでも内部を反復処理するための IEnumerator を返しているだけなので、 GetEnumerator() を呼び出しただけでは内部のメソッドは実行されません。 MoveNext() を呼び出して初めて、最初の yield return までが実行されます。

これは IEnumerator だけでなく IEnumerable でも同様の挙動になります。
IEnumerable<T> を返す…といえば、そう、LINQですね。LINQの SelectWhere が遅延評価なのは上記によるものです。

参考


  1. ジェネリクスな IEnumerator<T> IEnumerable<T> もありますが、本稿では非ジェネリクス版を用いています。 

  2. 正確には IEnumerable を実装していなくても、 MoveNext 関数 と Current プロパティを持った型を返す GetEnumerator 関数を持ってさえいれば foreach に指定することができます。(いわゆるダックタイピング) 

  3. この例では IEnumeratorTestforeach に指定出来る条件も満たしているため、 Start() 内で while 文の代わりに foreach (var value in this) { Debug.Log(value); } なんて書き方もできます。 

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
74
Help us understand the problem. What are the problem?