経緯
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); }
なんて書き方もできます。 ↩