3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-11-12

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

IEnumeratorの定義

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

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

単純なEnumerator

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

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);
    // }
}
stdout
0
1

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

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は使えなくなっているのですね。

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

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

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

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

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

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

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…?反復子のメソッドから生成されたクラスだな、元のメソッドを見てみることができます。

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?