C# 2.0 クイズ

RTM の公開も開始され,今後はいよいよ C# 2.0 のコードを目にする機会も増えてくることかと思います.さて今日は「C# 2.0 の準備できてますかチェック」ということで,いくつかクイズを出してみましょう.
元ネタは青柳さんの blog ですが,ネタばれを避けるためにリンクの紹介は解答と一緒に行っています.

第1問 以下のプログラムの実行結果を予想しなさい

using System;
using System.Collections.Generic;

class Program
{
    public delegate void MyFunction();
    static public IEnumerable<MyFunction> MyFuncs1()
    {
        List<MyFunction> funcs = new List<MyFunction>();
        for (int i=0; i < 10; ++i )
        {
            funcs.Add(delegate() { Console.Write("{0} ", i);});
        }
        return funcs;
    }

    static void Main(string[] args)
    {
        foreach (MyFunction v in MyFuncs1())
        {
            v();
        }
    }
}

第2問 以下のプログラムの実行結果を予想しなさい

using System;
using System.Collections.Generic;

public class Sequence
{
    public static IEnumerable<int> Range(int start, int count)
    {
        if (count < 0) throw new ArgumentException();
        for (int i = 0; i < count; i++) yield return start + i;
    }
}

class Program
{
    public delegate void MyFunction();
    static public IEnumerable<MyFunction> MyFuncs2()
    {
        List<MyFunction> funcs = new List<MyFunction>();
        foreach (int i in Sequence.Range(0, 10))
        {
            funcs.Add(delegate() { Console.Write("{0} ", i); });
        }
        return funcs;
    }

    static void Main(string[] args)
    {
        foreach (MyFunction v in MyFuncs2())
        {
            v();
        }
    }
}

第3問 以下のプログラムの実行結果を予想しなさい

using System;
using System.Collections.Generic;

public class Sequence
{
    public static IEnumerable<int> Range(int start, int count)
    {
        if (count < 0) throw new ArgumentException();
        for (int i = 0; i < count; i++) yield return start + i;
    }
}

class Program
{
    public delegate void MyFunction();
    static public IEnumerable<MyFunction> MyFuncs3()
    {
        foreach (int i in Sequence.Range(0, 10))
        {
            yield return delegate() { Console.Write("{0} ", i); };
        }
    }

    static void Main(string[] args)
    {
        foreach (MyFunction v in MyFuncs3())
        {
            v();
        }
    }
}

第4問 以下のプログラムの実行結果を予想しなさい

using System;
using System.Collections.Generic;

public class Sequence
{
    public static IEnumerable<int> Range(int start, int count)
    {
        if (count < 0) throw new ArgumentException();
        for (int i = 0; i < count; i++) yield return start + i;
    }
    public static T ToArray<T>(IEnumerable<T> source)
    {
        List<T> List = new List<T>();
        foreach (T item in source) List.Add(item);
        return List.ToArray();
    }
}

class Program
{
    public delegate void MyFunction();
    static public IEnumerable<MyFunction> MyFuncs3()
    {
        foreach (int i in Sequence.Range(0, 10))
        {
            yield return delegate() { Console.Write("{0} ", i); };
        }
    }
    static void Main(string args)
    {
        foreach (MyFunction v in Sequence.ToArray(MyFuncs3()))
        {
            v();
        }
    }
}

結果は……







第1問
10 10 10 10 10 10 10 10 10 10
第2問
9 9 9 9 9 9 9 9 9 9
第3問
0 1 2 3 4 5 6 7 8 9
第4問
9 9 9 9 9 9 9 9 9 9

最初にも書いたように,元ネタは青柳さんの以下のポストです.

まず,for 文の変数をキャプチャする Anonymous Method の挙動について.

for (int i=0; i < 10; ++i )
{
    funcs.Add(delegate() { Console.Write("{0} ", i);});
}

ここでの i は,for に入ってから脱出するまでの間ずっと使いまわされている「同一の変数」であるため,ループ脱出後に評価を行うと最後の値である 10 が返ります.これについては C# の Anonymous Method に問題があるというよりは,C# の for ループ文がそもそもそういうものなのでしょうがないという気がします(同様の指摘は青柳さんのポストからリンクされている元記事のコメントにも見られます).実際以下のように書き換えることで実行結果は"0 1 2 3 4 5 6 7 8 9"に変化します.

for (int i=0; i < 10; ++i )
{
    int j = i;
    funcs.Add(delegate() { Console.Write("{0} ", j);});
}

次に foreach 文の変数をキャプチャする場合.元記事にあるような Ruby との対比を行うにはこちらの方が適切でしょう.

foreach (int i in Sequence.Range(0, 10))
{
    funcs.Add(delegate() { Console.Write("{0} ", i);});
}

ややこしいことに,実はこちらも最初の for 文の時と同じ結果になります.詳細については青柳さんが既に書かれているので省略しますが,何でこういう仕様になっているのかは実は私も知りません.
次に yield による遅延評価版.

foreach (int i in Sequence.Range(0, 10))
{
    yield return delegate() { Console.Write("{0} ", i); };
}

ある i について yield return が行われた直後に Anonymous Method を実行した場合と(第3問),評価が次以降へ進んでしまってから Anonymous Method を実行した場合(第4問)で結果が変わります.
とまあ不安が残る方は,まずは目の前の C# 2.0 を片付けておく方がベターです.C# 3.0 の LINQ や lambda 式に手を出すのはその後でも遅くはないでしょう.