クロージャと変数のスコープ
クロージャの内部から参照された外部の変数
まずは以下のコードでどのような値がでるか予測してみてください。
String text = "晴れ";
Action md = () => Console.WriteLine(text);
md();
晴れという文字がコンソールに出力されます。では以下のようにするとどうなるでしょうか?
String text = "晴れ";
text = "曇り";
Action md = () => Console.WriteLine(text);
md();
これは曇りという文字が出力されます。では以下の場合はどうなるでしょうか?
String text = "晴れ";
Action md = () => Console.WriteLine(text);
text = "曇り";
md();
これは曇りという文字が出力されます。
Action md = () => Console.WriteLine(text);
のときにtextに入っている晴れという値が出力されるのではなく実行時の
md();
のときにtextに入っている値(曇り)が出力されることになります。 実行時に入っている値が使用されるということに注意してください。
実際にはまるパターン
上記の例だと注意していれば大丈夫な気がしますが以下のような場合だとはまりがちです。 ユーザーの一覧を取得しユーザーごとに複数のスレッドで処理を実行させることを考えて見ます。
static void Main(string[] args)
{
    var taskList = GetTaskList();
    for (int i = 0; i < taskList.Count; i++)
    {
        taskList[i].Start();
    }    
}
static List<Task> GetTaskList()
{
    List<Task> tt = new List<Task>();
    Task t = null;
    var userList = GetUserList(); //4人のユーザーが取得される
    for (int i = 0; i < userList.Count; i++)
    {
        t = new Task(() => Console.WriteLine(userList[i].UserName));
        tt.Add(t);
    }
    return tt;
}
static List<User> GetUserList()
{
    List<User> l = new List<User>();

    l.Add(new User("Jenifer", 20));
    l.Add(new User("Bolt", 26));
    l.Add(new User("Messi", 25));
    l.Add(new User("Pele", 60));

    return l;
}
何の問題もなく実行されそうに見えますが実際には以下の例外が発生します。
何故例外が発生するのかというと
t = new Task(() => Console.WriteLine(userList[i].UserName));
の部分が実際に実行されるのは
taskList[i].Start();
となります。この時点ではGetTaskListの内部の i の値は4になっているはずです。 userListのインデックスは0-3までしか駄目なのでArgumentOutOfRangeExceptionが発生します。
上記のコードは以下のように書き直すと正しく実行されます。
static void Main(string[] args)
{
    var taskList = GetTaskList();
    for (int i = 0; i < taskList.Count; i++)
    {
        taskList[i].Start();
    }    
}
static List<Task> GetTaskList()
{
    List<Task> tt = new List<Task>();
    Task t = null;
    var userList = GetUserList();
    for (int i = 0; i < userList.Count; i++)
    {
        var rUser = userList[i];
        t = new Task(() => Console.WriteLine(rUser.UserName));
        tt.Add(t);
    }
    return tt;
}
ユーザークラスの定義は以下です。
public class User
{
    public String UserName { get; set; }
    public Int32 Age { get; set; }
    public User(String userName, Int32 age)
    {
        this.UserName = userName;
        this.Age = age;
    }
}
ポイントは以下の2行です。
var rUser = userList[i];
t = new Task(() => Console.WriteLine(rUser.UserName));
これによりfor文で繰り返した数と同じだけ変数が宣言されその変数にそれぞれのユーザーオブジェクトが代入されます。 実行すると正しく結果が表示されます。
Create at 2012/1/22 LastUpdate 2013/1/21