Как lambda в С# связывается с перечислителем в foreach?
Я натолкнулся на самое неожиданное поведение. Я уверен, что есть веская причина, по которой это работает. Может кто-нибудь объяснить это?
Рассмотрим этот код:
var nums = new int[] { 1, 2, 3, 4 };
var actions = new List<Func<int>>();
foreach (var num in nums)
{
actions.Add(() => num);
}
foreach (var num in nums)
{
var x = num;
actions.Add(() => x);
}
foreach (var action in actions)
{
Debug.Write(action() + " ");
}
Результат для меня немного удивителен:
4 4 4 4 1 2 3 4
Очевидно, что что-то происходит с тем, как лямбда ссылается на счетчик. В первой версии foreach есть 'num', фактически связанный с 'Current', вместо результата, возвращаемого им?
Ответы
Ответ 1
Это хорошо известное и установленное поведение в отношении лямбда, хотя часто это удивительно для тех, кто впервые столкнулся с ним. Основная проблема заключается в том, что ваша ментальная модель того, что такое лямбда, не совсем корректна.
Lambda - это функция, которая не запускается до ее вызова. Ваше закрытие связывает ссылку на этот экземпляр лямбды, а не на значение. Когда вы выполняете свои действия в своем последнем цикле foreach, это первый раз, когда вы на самом деле следуете закрытой ссылке, чтобы узнать, что это такое.
В первом случае вы ссылаетесь на num, и в этот момент значение num равно 4, поэтому, конечно, весь ваш результат равен 4. Во втором случае каждая лямбда была привязана к другому значению, которое был локальным для цикла каждый раз, и это значение не изменяется (оно не было GC'd исключительно из-за ссылки лямбда.), поэтому вы получаете ответ, который вы ожидаете.
Закрытие локального временного значения на самом деле является стандартным подходом к захвату определенного значения из точки времени в пределах лямбда.
Адам ссылка на блог Eric Lippert содержит более подробное (и технически точное) описание того, что происходит.
Ответ 2
Смотрите сообщение в блоге Эрика Липперта по этому вопросу; это связано с тем, как переменные итератора > в коде, и как это относится к закрытию лямбда-крыльев и поднятым функциям.
Ответ 3
Так как конструкция foreach
- это просто синтаксический сахар, лучше всего думать об этом в этой истинной форме.
int num;
while (nums.MoveNext())
{
num = nums.Current;
actions.Add(() => num);
}
Лямбда будет захватывать переменную num
, поэтому при выполнении лямбда будет использоваться последнее значение num
.
Ответ 4
Это происходит из-за двух следующих вещей:
1) делегаты сохраняют контекст (область) внешних переменных
2) первый цикл foreach будет скомпилирован только в объявленной переменной "num".
3) ленивая оценка
Каждый делегированный, добавленный в первый цикл, сохранит ту же самую переменную num, сохраненную в области. Из-за ленивой оценки вы будете запускать делегатов после завершения первого цикла, поэтому num veraible, сохраненный в области делегатов, равен 4.
Ответ 5
См. О lambdas, захвате и изменчивости