Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8899

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

$
0
0

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);// }}
stdout
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を使うことにします。lockcheckもスコープを抜ける際に特定の(.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…?反復子のメソッドから生成されたクラスだな、元のメソッドを見てみることができます。

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


Viewing all articles
Browse latest Browse all 8899

Trending Articles