えんじにあ雑記!

開発していて学んだことをまとめていきます!

Unityのコルーチン 〜IEnumeratorのその先へ〜

f:id:flat-M_M:20200308011751j:plain

Unityで頻繁に用いるコルーチン。

その返り値の型として使われるIEnumerator。こいつ一体何者なんだ?

正体を探る旅に出た。

はじめに

Unityでコルーチンを使っていて、ハマったところがあったのをきっかけに

「そもそも、IEnumeratorを返す関数ってなんなのよ🤔」

って思ったので調査しました。

よくある例

private IEnumerator Coroutine() {
    Console.WriteLine("なんかの前処理");
    yield return null;
    Console.WriteLine("なんかの後処理");
    yield return null;
}

こんな感じでyieldキーワードを使って複数回リターンしているような関数をコルーチンとしてUnityでは使うことがあります。

(今回このサンプルコード自体に意味はないのでてきとうです。)

Coroutine()は何をしてるのか

上記にあげたサンプルコードで結局Coroutine()メソッドは内部的に何をしているのか。

答えを先に述べると

Coroutine()メソッドは状態遷移機械を生成して返しているだけ

になります。

上記のコードをデコンパイルして、内部的にどう実装されているかを確認します。

[CompilerGenerated]
    private sealed class <Coroutine>d__0 : IEnumerator<object>, IDisposable, IEnumerator
    {
        private int <>1__state;

        private object <>2__current;

        public Main <>4__this;

        object IEnumerator<object>.Current
        {
            [DebuggerHidden]
            get
            {
                return <>2__current;
            }
        }

        object IEnumerator.Current
        {
            [DebuggerHidden]
            get
            {
                return <>2__current;
            }
        }

        [DebuggerHidden]
        public <Coroutine>d__0(int <>1__state)
        {
            this.<>1__state = <>1__state;
        }

        [DebuggerHidden]
        void IDisposable.Dispose()
        {
        }

        private bool MoveNext()
        {
            switch (<>1__state)
            {
                default:
                    return false;
                case 0:
                    <>1__state = -1;
                    Console.WriteLine("なんかの前処理");
                    <>2__current = null;
                    <>1__state = 1;
                    return true;
                case 1:
                    <>1__state = -1;
                    Console.WriteLine("なんかの後処理");
                    <>2__current = null;
                    <>1__state = 2;
                    return true;
                case 2:
                    <>1__state = -1;
                    return false;
            }
        }

        bool IEnumerator.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            return this.MoveNext();
        }

        [DebuggerHidden]
        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }
    }

    [IteratorStateMachine(typeof(<Coroutine>d__0))]
    private IEnumerator Coroutine()
    {
        <Coroutine>d__0 <Coroutine>d__ = new <Coroutine>d__0(0);
        <Coroutine>d__.<>4__this = this;
        return <Coroutine>d__;
    }
}

まず、Coroutine()メソッドの中身を見てみると、<Coroutine>d__0クラスのインスタンスを生成して返しているだけになります。

[IteratorStateMachine(typeof(<Coroutine>d__0))]
private IEnumerator Coroutine()
{
    <Coroutine>d__0 <Coroutine>d__ = new <Coroutine>d__0(0);
    <Coroutine>d__.<>4__this = this;
    return <Coroutine>d__;
}

なので、自動生成された<Coroutine>d__0クラスを覗いてみることにします。

<Coroutine>d__0クラスとは

先ほども言ったとおり、Coroutine()メソッドは

状態遷移機械を生成して返しているだけ

になります。

そして、その状態遷移機械にあたるのがこの<Coroutine>d__0クラスになります。

それほど複雑な実装ではないため、ある程度読めば雰囲気がわかると思いますが、ステートマシーンとして機能している部分がMoveNext()メソッドになります。

MoveNext()の実装内容

MoveNext()メソッドが行っていることは次の通りになります。

  1. <>1__state変数の値に応じて分岐処理
  2. <>1__stateの値を変更する
  3. <>2__currentの値を変更する
  4. bool値を返す

まず、コンストラクタで<>1__stateは0で初期化されているため、MoveNextが初めて呼ばれたタイミングで実行される部分は下記のコードになります。

case 0:
    <>1__state = -1;
    Console.WriteLine("なんかの前処理");
    <>2__current = null;
    <>1__state = 1;
    return true;

Coroutine()メソッドに元々定義していた処理が実行されていることがわかります。

また、<>2__currentの値を書き換えていることもわかります。

そして、yield return で返している値は内部的にはcurrentに格納されていることがわかります。

C#でパフォーマンスの良いコードを書く記事のforeachのデコンパイル結果でもEnumeratorのCurrentにアクセスするところがあります。 こう言った仕組みで値が代入されているという発見がありますね。

そして最後に<>1__stateの値を次に進めています。ここが状態遷移機械らしい動作をしている部分になります。

次にMoveNextが呼ばれた時に実行されるコードは下記になります。

case 1:
    <>1__state = -1;
    Console.WriteLine("なんかの後処理");
    <>2__current = null;
    <>1__state = 2;
    return true;

ここでも同じような処理を実行しています。

ではもう一度MoveNext()メソッドを実行した場合を考えます。

case 2:
    <>1__state = -1;
    return false;

ステートマシーンが遷移を終了したことがわかります。<>1__stateが-1になったため、今後MoveNext()が呼び出された場合、default句を通り続けるようになるため、MoveNext()はfalseを返します。

つまり、自動生成されたd__0クラスは終了も含めると3状態を持つ状態遷移機械を表現したクラスになるということがわかりました。

まとめ

  1. IEnumeratorを返り値に持つメソッド(コルーチン)は内部的には状態遷移機械を表現するクラスのインスタンスを生成して返しているだけ
  2. 自動生成されたクラスによって、yield returnで返される値は管理されている

今回、コルーチンの他の部分で詰まって、結果的にIEnumeratorについて調査したので備忘録的に記事にしてみました。

なんとなく使うより、内部の仕組みを理解して使えるほうが楽しいですね笑

理解のヒントになれれば幸いです🙇‍♂️