経緯
UnityでC#を用いた開発などを行っているとよく IEnumerator と IEnumerable というインターフェース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の Select や Where が遅延評価なのは上記によるものです。
参考
- IEnumerator Interface (System.Collections) | Microsoft Docs
- IEnumerable Interface (System.Collections) | Microsoft Docs
- C# foreach ステートメント | Microsoft Docs
- [コンテキスト キーワード yield - C# リファレンス | Microsoft Docs]
(https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/yield) - 配列 - C# プログラミング ガイド | Microsoft Docs
- ステートメントStatements | Microsoft Docs
- Iterators | Microsoft Docs
- コルーチン - Unity マニュアル
- foreach - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
-
ジェネリクスな
IEnumerator<T>IEnumerable<T>もありますが、本稿では非ジェネリクス版を用いています。 ↩ -
正確には
IEnumerableを実装していなくても、MoveNext関数 とCurrentプロパティを持った型を返すGetEnumerator関数を持ってさえいればforeachに指定することができます。(いわゆるダックタイピング) ↩ -
この例では
IEnumeratorTestはforeachに指定出来る条件も満たしているため、Start()内でwhile文の代わりにforeach (var value in this) { Debug.Log(value); }なんて書き方もできます。 ↩