コードジェネレータとしての 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");
    }
}

このコードは,以下のように振る舞うと考えれば良いでしょう.

  1. 初めて MoveNext() が呼ばれたとき,イテレータ先頭から最初の yield 文までが実行される.
  2. 初回以降は,MoveNext() が呼ばれるたびに,前回停止した yield 文直後から次の yield 文までが実行される.
  3. 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() が実行されたときということになります.