C#にはイテレータ(IEnumerator)が簡単で直感的に記述できる構文があります。
代表的な使い方の一つに、Unityではコルーチンを記述するために使用されていますが、元々はリストのような値を順番に取り出せるものを記述するためにあります。
このイテレータ構文が実際にはどのような動作をするのか、C#とILを実際に見比べながら確かめてみます。(コンパイルは.NETCore3.0)
IEnumeratorの定義
interfaceIEnumerator{voidReset();boolMoveNext();// falseの時、終了objectCurrent{get;}// object型ではなく、任意の型を返せるIEnumerator<T>もあります。}
これを反復子(yield return
)を使用せずに記述しようとすると、新しくクラスを作成し、この三つのメンバを実装しなければなりません。
一方で、反復子を使ったメソッドでは、returnするのはobject型であり(Generic版であればT型)IEnumeratorをreturnする記述はしません。
ILには反復子の命令はありませんので反復子で記述されたメソッドを上記のインターフェイスを実装するクラスに書き下す必要があります。(コンパイラがやってくれます)
単純なEnumerator
例えば次のような例を見てみましょう。
classRepeat0To1:IEnumeratable{// 0~1を順番に返すイテレータpublicIEnumeratorGetEmumerator(){yieldreturn0;yieldreturn1;}}voidMain(string[]args){foreach(objectiteminnewRepat0To1){Console.WriteLine(item);}// foreachはコンパイラによって以下のように書き下される// IEnumerator iterator = new Repat0To1().GetEnumerator();// while(iterator.MoveNext())// {// Console.WriteLine(iterator.Current);// }}
0
1
これをコンパイルすると次のようになります。ILで書くと長いので、C#に書き起こしています。
classRepeat0To1: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;publicobjectCurrent=><>2__current;publicboolMoveNext(){switch(<>1__state){case0:{<>1__state=-1;<>2__current=0;<>1__state=1returntrue;}case1:{<>1__state=-1;<>2__current=0;<>1__state=2returntrue;}default{<>1__state=-1;returnfalse;}}}publicvoidReset()=>thrownewNotSupportedException();}publicIEnumeratorGetEmumerator()=>new<Iterator1>d_1(0);}
MoveNext
は想像に難くない感じですが、Reset
は使えなくなっているのですね。
ローカル変数と引数、そして可変長
publicIEnumeratorGetEmumerator(string[]args){for(inti=0;i<args.Length;i++){yieldreturnargs[i];}}
さらにメソッドを複雑にしてみます。次のメソッドでは、引数とローカル変数を使用しています。また、先ほどのとは異なり終了までの回数があらかじめわかっていません。これをコンパイルすると次のようになります。ILで書くと長いので、C#に書き起こしています。
class<Iterator1>d_1:IEnumerator{int<>1__state;object<>2__current;int<i>5__2;publicstring[]args;// public<Iterator1>d_1(int<>1__state)=>this.<>1__state=<>1__state;publicobjectCurrent=><>2__current;publicboolMoveNext(){switch(<>1__state){case0:// 1回目{<>1__state=-1;<i>5__2=0;}case1:// 2~args.Length回目{<>1__state=-1;<i>5__2++;<>1__state=2returntrue;}default:// args.Length+1回目以降{returnfalse;}}if(<i>5__2<args.Length){<>2__current=args[<i>5__2];<>1__state=1;returntrue;}else{returnfalse;}}publicvoidReset()=>thrownewNotSupportedException();}publicIEnumeratorGetEmumerator(string[]args){IEnumeratoriterator=new<Iterator1>d_1(0);iterator.args=args;returniterator;}
引数や、ローカル変数はクラスのメンバにキャプチャされます。反復子をまたぐローカル変数を使いまくるとモリモリとヒープが肥大化するので注意が必要です。
イテレータの回数が決まっていないので、switchによるジャンプはできず、条件式を評価するようになりました。しかし、一度終了した後は条件式を評価せず、終了したことがキャッシュされるようです。
スコープの終了で処理の入る構文をつかう(using
,lock
)
staticIEnumeratorGetEmumerator(){using(newMemoryStream()){yieldreturnnull;}}
using
の方が単純なのでusing
を使うことにします。lock
やcheck
もスコープを抜ける際に特定の(.Netの)メソッドコールがされるので実質的には同じものと思われます。
コンパイルすると次のように書き下されます。
class<Iterator1>d_1:IEnumerator{int<>1__state;object<>2__current;IDispose<>7__wrap1;void<>m__Finally1()=><>7__wrap1.Dispose();voidDispose=><>m__Finally1();public<Iterator1>d_1(int<>1__state)=>this.<>1__state=<>1__state;publicobjectCurrent=><>2__current;publicboolMoveNext(){boolresult;switch(<>1__state){case0:{try{<>1__state=-1;<>7__wrap1=newMemoryStream();<>1__state=-3;<>2__current=0;<>1__state=1;result=true;}catch{Dispose();}}case1:{try{<>1__state=-3;<>m__Finally1();// -> <>7__wrap1.Dispose()<>7__wrap1=null;result=false;}catch{Dispose();// -> <>m__Finally1() -> <>7__wrap1.Dispose()}}default:{result=false;}}returnresult;}publicvoidReset()=>thrownewNotSupportedException();}publicIEnumeratorGetEmumerator()=>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されるようになります。