C#の反復子（yeild return）はコンパイラでどのように書き下されるのか？

C#にはイテレータ（IEnumerator）が簡単で直感的に記述できる構文があります。
代表的な使い方の一つに、Unityではコルーチンを記述するために使用されていますが、元々はリストのような値を順番に取り出せるものを記述するためにあります。
このイテレータ構文が実際にはどのような動作をするのか、C#とILを実際に見比べながら確かめてみます。（コンパイルは.NETCore3.0）

# IEnumeratorの定義

```csharp
interface IEnumerator
{
   void Reset();
   bool MoveNext(); // falseの時、終了
   object Current { get; }
   // object型ではなく、任意の型を返せるIEnumerator<T>もあります。
}
```

これを反復子（`yield return`）を使用せずに記述しようとすると、新しくクラスを作成し、この三つのメンバを実装しなければなりません。
一方で、反復子を使ったメソッドでは、returnするのはobject型であり（Generic版であればT型）IEnumeratorをreturnする記述はしません。
ILには反復子の命令はありませんので反復子で記述されたメソッドを上記のインターフェイスを実装するクラスに書き下す必要があります。（コンパイラがやってくれます）

# 単純なEnumerator

例えば次のような例を見てみましょう。

```csharp
class Repeat0To1: IEnumeratable
{
    // 0～1を順番に返すイテレータ
    public IEnumerator GetEmumerator()
    {
        yield return 0;
        yield return 1;
    }
}

void Main(string[] args)
{
    foreach(object item in new Repat0To1)
    {
        Console.WriteLine(item);
    }

    // foreachはコンパイラによって以下のように書き下される
    // IEnumerator iterator = new Repat0To1().GetEnumerator();
    // while(iterator.MoveNext())
    // {
    //     Console.WriteLine(iterator.Current);
    // }
}
```
```text:stdout
0
1
```

これをコンパイルすると次のようになります。ILで書くと長いので、C#に書き起こしています。

```csharp
class Repeat0To1: IEnumeratable
{
    // <>はGenericではなく、名前。ILでは<>が名前に使えます。
    class <Iterator1>d_1 : IEnumerator
    {
        int <>1__state;
        object <>2__current;

        public <Iterator1>d_1(int <>1__state) => this.<>1__state = <>1__state;
        public object Current => <>2__current;
        public bool MoveNext()
        {
            switch(<>1__state)
            {
                case 0:
                {
                    <>1__state = -1;
                    <>2__current = 0;
                    <>1__state = 1
                    return true;
                }
                case 1:
                {
                    <>1__state = -1;
                    <>2__current = 0;
                    <>1__state = 2
                    return true;
                }
                default
                {
                    <>1__state = -1;
                    return false;
                }
            }
        }
        public void Reset() => throw new NotSupportedException();

    }
    public IEnumerator GetEmumerator() => new <Iterator1>d_1(0);
}
```

`MoveNext`は想像に難くない感じですが、`Reset`は使えなくなっているのですね。

# ローカル変数と引数、そして可変長

```csharp
public IEnumerator GetEmumerator(string[] args)
{
    for (int i = 0; i < args.Length; i++)
    {
        yield return args[i];
    }
}
```

さらにメソッドを複雑にしてみます。次のメソッドでは、引数とローカル変数を使用しています。また、先ほどのとは異なり終了までの回数があらかじめわかっていません。これをコンパイルすると次のようになります。ILで書くと長いので、C#に書き起こしています。

```csharp
class <Iterator1>d_1 : IEnumerator
{
    int <>1__state;
    object <>2__current;
    int <i>5__2;
    public string[] args; // 
    
    public <Iterator1>d_1(int <>1__state) => this.<>1__state = <>1__state;
    public object Current => <>2__current;
    public bool MoveNext()
    {
        switch(<>1__state)
        {
            case 0: // 1回目
            {
                <>1__state = -1;
                <i>5__2 = 0;
            }
            case 1: // 2～args.Length回目
            {
                <>1__state = -1;
                <i>5__2++;
                <>1__state = 2
                return true;
            }
            default: // args.Length+1回目以降
            {
                return false;
            }
        }

        if(<i>5__2 < args.Length)
        {
            <>2__current = args[<i>5__2];
            <>1__state = 1;
            return true;
        }
        else
        {
            return false;
        }
    }
    public void Reset() => throw new NotSupportedException();
}

public IEnumerator GetEmumerator(string[] args)
{
    IEnumerator iterator = new <Iterator1>d_1(0);
    iterator.args = args;
    return iterator;
}
```

引数や、ローカル変数はクラスのメンバにキャプチャされます。反復子をまたぐローカル変数を使いまくるとモリモリとヒープが肥大化するので注意が必要です。
イテレータの回数が決まっていないので、switchによるジャンプはできず、条件式を評価するようになりました。しかし、一度終了した後は条件式を評価せず、終了したことがキャッシュされるようです。

# スコープの終了で処理の入る構文をつかう（`using`,`lock`）

```csharp
static IEnumerator GetEmumerator()
{
    using(new MemoryStream())
    {
        yield return null;
    }
}
```

`using`の方が単純なので`using`を使うことにします。`lock`や`check`もスコープを抜ける際に特定の（.Netの）メソッドコールがされるので実質的には同じものと思われます。
コンパイルすると次のように書き下されます。

```csharp
class <Iterator1>d_1 : IEnumerator
{
    int <>1__state;
    object <>2__current;
    IDispose <>7__wrap1;
    void <>m__Finally1() => <>7__wrap1.Dispose();
    void Dispose => <>m__Finally1();
    
    public <Iterator1>d_1(int <>1__state) => this.<>1__state = <>1__state;
    public object Current => <>2__current;
    public bool MoveNext()
    {
        bool result;
        switch(<>1__state)
        {
            case 0:
            {
                try
                {
                    <>1__state = -1;
                    <>7__wrap1 = new MemoryStream();
                    <>1__state = -3;
                    <>2__current = 0;
                    <>1__state = 1;
                    result = true;
                }
                catch
                {
                    Dispose();
                }
            }
            case 1:
            {
                try
                {
                    <>1__state = -3;
                    <>m__Finally1(); // -> <>7__wrap1.Dispose()
                    <>7__wrap1 = null;
                    result = false;
                }
                catch
                {
                    Dispose(); // -> <>m__Finally1() -> <>7__wrap1.Dispose()
                }
            }
            default:
            {
                result = false;
            }
        }
        return result;
    }
    public void Reset() => throw new NotSupportedException();
}

public IEnumerator GetEmumerator() => new <Iterator1>d_1(0);
```

`<>7__wrap1.Dispose`や、`<Iterator1>d_1.Dispose`を経由してややこしくなっています。`MemoryStream.Dispose`はイテレータが終了する時の`MoveNext`または、毎`MoveNext`で例外が出たときに呼ばれます。つまり、必ず呼ばれる仕組みになっています。イテレータがちゃんと最後まで実行されれば…という前提ですが。

# まとめ

C#の反復子は便利でちゃんと直感的な挙動をしてくれるようになっているようです。ファイルを一行づつ読んでいって…（`using(StreamReader)`）なども安心して使えます。
また、メソッドはクラスにコンパイルされるので、`<>c__Iterator0.MoveNext null refarence exception`みたいな知らないメソッドでエラーが出ても`<>c_Iterator0`…？反復子のメソッドから生成されたクラスだな、元のメソッドを見てみることができます。

ちなみに、`fixed`や`stackalloc`、つまり`unsafe`と`yield return`は併用できませんので検証していません。
`try-catch-finally`はILレベルでサポートされています。これもそれぞれの`MoveNext`でtryされるようになります。