コードジェネレータとしての C# イテレータ
前回 (id:NyaRuRu:20070331:p1) C# のイテレータで「列挙前」の処理を書ける,つまり Enumerator のコンストラクタに実装コードを流し込めると書きましたが,たぶんそういう事実は無いので訂正しておきます.
どういうことかを示すために,次のようなコードを考えましょう.
static IEnumerable<int> Test() { try { Trace.WriteLine("try Level 0"); yield return 0; try { Trace.WriteLine("try Level 1"); yield return 1; try { Trace.WriteLine("try Level 2"); yield return 2; } finally { Trace.WriteLine("finally Level 2"); } } finally { Trace.WriteLine("finally Level 1"); } } finally { Trace.WriteLine("finally Level 0"); } }
このコードは,以下のように振る舞うと考えれば良いでしょう.
- 初めて MoveNext() が呼ばれたとき,イテレータ先頭から最初の yield 文までが実行される.
- 初回以降は,MoveNext() が呼ばれるたびに,前回停止した yield 文直後から次の yield 文までが実行される.
- yield break やイテレータ終端にたどり着いたり,外部から Dispose() が呼ばれた場合は,現在の yield 文直後から return で脱出したかのように finally 句が順番に実行される.
これを確かめてみます.
static void Main(string[] args) { IEnumerable<int> enumerable = Test(); //(何も表示されていない) IEnumerator<int> e = enumerable.GetEnumerator(); //(何も表示されていない) e.MoveNext(); //(次の文が表示されている) // "try Level 0" e.MoveNext(); //(次の文が表示されている) // "try Level 0" // "try Level 1" e.Dispose(); //(次の文が表示されている) // "try Level 0" // "try Level 1" // "finally Level 1" // "finally Level 0" }
これがつまり,「列挙前」のコードを書けないという意味です.
C# イテレータに書いたコードが初めて実行されるのは,GetEnumerator() 実行時ではなく,初めて IEnumerator<T>.MoveNext() が実行されたときということになります.