少し前まで、経験の浅い同僚から、C#でどの利回りリターンが使用されているのかと尋ねられました。 私はイテレーターをあまり頻繁に書かないので、それに答えて、私の言葉を疑った。 MSDNに対処した後、私は言われたことを強化しましたが、「この命令は何にコンパイルされますか?」という質問がありました。 」 この記事は古くなっていますが、ロシア語の記事や文書を読むことに慣れている開発者の特定のグループにとって役立つと思います。
続行するリンク: C#でのイテレーターの実装(パート2)
無名メソッドのように、C#のイテレータは複雑な構文糖です。 それらを完全に自分で実装できます(結局、以前のバージョンのCではこれを行う必要がありました)が、コンパイラを使用する方がはるかに便利です。 イテレータの背後にある考え方は、関数がyield return式(および、場合によってはyield break式)を使用して、それをステートマシンに変換することです。 yield returnが呼び出されると、関数の状態が保存され、次のオブジェクトを取得するためにイテレーターに再度アクセスすると、この状態が復元されます。 イテレータの主なことは、すべてのローカルイテレータ変数(事前に初期化されたローカル変数としてのイテレータパラメータを含み、非表示のthisパラメータを含む)が補助クラスのメンバ変数(以降フィールド)になることです。 さらに、補助クラスには、実行が中断された場所を追跡する状態フィールドと、すでにリストされているオブジェクトの最新のものを格納する現在のフィールドが含まれます。
class MyClass { int limit = 0; public MyClass(int limit) { this.limit = limit; } public IEnumerable<int> CountFrom(int start) { for (int i = start; i <= limit; i++) yield return i; } }
CountFromメソッドは、ステップ1で開始から制限までの整数を生成する整数列挙子を作成します。コンパイラはこの列挙子を暗黙的に次のようなものに変換します。
class MyClass_Enumerator : IEnumerable<int> { int state$0 = 0; // int current$0; // MyClass this$0; // CountFrom int start; // CountFrom int i; // CountFrom public int Current { get { return current$0; } } public bool MoveNext() { switch (state$0) { case 0: goto resume$0; case 1: goto resume$1; case 2: return false; } resume$0:; for (i = start; i <= this$0.limit; i++) { current$0 = i; state$0 = 1; return true; resume$1:; } state$0 = 2; return false; } // ... , ... } public IEnumerable<int> CountFrom(int start) { MyClass_Enumerator e = new MyClass_Enumerator(); e.this$0 = this; e.start = start; return e; }
列挙クラスはコンパイラによって自動的に生成され、約束どおり、状態と現在のオブジェクトのフィールドに加えて、ローカル変数ごとに1つのフィールドが含まれます。 Currentプロパティは、単に現在のオブジェクトを返します。 実際の作業はすべて、MoveNextメソッドで行われます。
MoveNextメソッドを生成するために、コンパイラは作成したコードを受け取り、いくつかの変換を実行します。
- コードはヘルパークラスに移植されているため、すべての変数参照を調整する必要があります。
- これはこの$ 0になります。これは、この生成された関数内で、オリジナルの代わりに自動的に生成されたクラスを指すためです。
- mが元のクラス(非静的フィールド、プロパティ、またはメソッド)のメンバーである場合、mはこの$ 0.mになります。 実際、このルールは前のルールと不必要に組み合わされています。 接頭辞mなしでクラスメンバーの名前を記述することは、this.mの省略形です。
- vがパラメーターまたはローカル変数の場合、vはthis.vになります。 このルールも不要です。なぜなら vエントリはthis.vに相当しますが、変数ストレージが変更されたことに気付くように明示的に参照しています。
さらに、コンパイラーはすべてのyield returnステートメントを処理する必要があります。
各yield return x式はに変換されます
current$0 = x; state$0 = n; return true; resume$n:;
ここで、nは1から始まる数字です。
さらに、yield break式があります。
各yield break式はに変換されます
state$0 = n2; return false;
ここで、n2はyield return式で使用されるすべての状態の最大数よりも大きい1です。 各関数の最後に、ブレークを生成する呼び出しが暗黙的に含まれていることを忘れないでください。
最後に、コンパイラーは関数の最初に大きな状態マネージャーを挿入します。
switch (state$0) { case 0: goto resume$0; case 1: goto resume$1; case 2: goto resume$2; // ... case n: goto resume$n; case n2: return false; }
各状態に対して1つのcase-expressionが作成され、初期状態と最終状態に対してn2が追加されます。