[C#]IEnumeratorとIEnumerableを調べた

# 経緯

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

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

# `IEnumerator` とは

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

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

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

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

# `IEnumerable` とは

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

```cs
IEnumerator GetEnumerator();
```

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

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

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

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

# `foreach` は糖衣構文

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

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

```cs
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]

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

```cs
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](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.ienumerator?view=netframework-4.7.2)
- [IEnumerable Interface (System.Collections) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.ienumerable?view=netframework-4.7.2)
- [C# foreach ステートメント | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/foreach-in)
- [コンテキスト キーワード yield - C# リファレンス | Microsoft Docs]
(https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/yield)
- [配列 - C# プログラミング ガイド | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/arrays/)
- [ステートメントStatements | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/language-specification/statements)
- [Iterators | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/iterators)
- [コルーチン - Unity マニュアル](https://docs.unity3d.com/ja/current/Manual/Coroutines.html)
- [foreach - C# によるプログラミング入門 | ++C++; // 未確認飛行 C](https://ufcpp.net/study/csharp/sp_foreach.html)