Замыкание в C#: лямбда в цикле захватывает не то значение переменной
Пытаюсь создать список лямбд (анонимных методов) в цикле, каждая должна печатать свой номер. Но когда я их потом вызываю, все печатают одно и то же число! Похоже на какую-то магию с захватом переменных, но не пойму, как это исправить.
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var a in actions)
a();
Я ожидал 0 1 2, а в старом проекте получаю 3 3 3. Что происходит и как сделать, чтобы каждая лямбда помнила своё число?
2 ответа
Это классический эффект замыкания: лямбда захватывает не значение переменной, а саму переменную i. Все три лямбды ссылаются на одну и ту же i. К моменту вызова цикл уже закончился и в i лежит 3 — поэтому 3 3 3.
Исправление — завести новую переменную внутри тела цикла на каждой итерации, тогда у каждой лямбды будет своя копия:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i; // новая переменная на каждой итерации
actions.Add(() => Console.WriteLine(copy));
}
foreach (var a in actions)
a(); // теперь 0 1 2
Кстати, в foreach (а не в for) и в C# 5+ для переменной цикла foreach эту проблему уже починили на уровне языка. А вот для классического for с i ловушка осталась — нужна явная копия.
Дополню, что именно «захватывается». Компилятор под капотом создаёт скрытый класс, в поле которого лежит переменная i, а лямбды держат ссылку на один общий объект этого класса. Поэтому они и видят одно значение.
Когда ты объявляешь int copy = i; внутри цикла, на каждой итерации создаётся новый такой объект со своей copy — и захваты перестают пересекаться. То же самое можно получить, если переписать for на foreach по диапазону:
foreach (int i in Enumerable.Range(0, 3))
actions.Add(() => Console.WriteLine(i)); // здесь уже 0 1 2 без копии
Но если остаёшься на for — вариант с локальной копией из принятого ответа самый надёжный.