UnityのC#でforeachの変数をクロージャで束縛すると共通の値が束縛されてしまう

2015-01-08

コールバックなど、なんらかのクロージャを使用する処理があったとして:

using System;
using System.Collections.Generic;

...
private List<Action> callbacks = new List<Action>();
void AddCallback(Action callback) {
callbacks.Add(callback);
}
void RunCallbacks() {
callbacks.ForEach((callback) => callback());
}

C#でforeachループで用いる要素の変数を束縛すると、値はすべてループが終わったときの値になってしまう:

int[] table = {1, 2, 3};
// Bad
foreach (var x in table) {
AddCallback(() => { Debug.Log(x); });
}
RunCallbacks(); //=> 3, 3, 3

まあよく考えると当然で、ループの変数はループ本体で共有する1つの変数であって、それをキャプチャしているのでそうなるのも頷ける:

// 上のものは内容的にはこんな感じ
int x;
for (int i = 0; i < table.Length; ++i) {
x = table[i];
AddCallback(() => { Debug.Log(x); });
}

そうではなくて個々の値をキャプチャする場合にはループ内部で別の変数に代入してやって、クロージャ内からはそれを参照するようにすれば別々の値がキャプチャされることになる:

// Good
foreach (var x in table) {
var y = x; // <= これが必要
AddCallback(() => { Debug.Log(y); }); // <= xでなくyを参照すること
}
RunCallbacks(); //=> 1, 2, 3

Suck!


  • 2016/10/12: Unity5.5.0b6で試したところ、解消されて個々の値が束縛されるようになった!
  • しかし有効なのは foreach だけで、 for のループ変数は相変わらずなのであった…
for (var i = 0; i < table.Length; ++i) {
var j = i;
AddCallback(() => { Debug.Log("i=" + i + ", j=" + j); });
}