Я просто столкнулся с самым неожиданным поведением. Я уверен, что существует серьезное основание, оно прокладывает себе путь. Может кто-то помогать объяснить это?
Рассмотрите этот код:
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 'цифра' на самом деле связывается с 'Текущим' вместо результата, возвращенного им?
Это хорошо известное и устоявшееся поведение ламбд, хотя часто удивляет тех, кто сталкивается с ним впервые. Основная проблема заключается в том, что ваша мысленная модель того, что такое лямбда это , не совсем верна.
Лямбда - это функция, которая не выполняется, пока ее не вызовут. Ваше закрытие связывает ссылку на экземпляр лямбды, а не значение. Когда вы выполняете свои действия в последнем цикле foreach, вы в первый раз на самом деле следуете за закрытой ссылкой, чтобы увидеть, что это такое.
В первом случае вы ссылаетесь на num, и в этот момент значение num равно 4, поэтому, конечно, все ваши выходные данные равны 4. Во втором случае каждая лямбда была привязана к другому значению, которое каждый раз было локальным для цикла, и это значение не изменилось (оно не было GC'd только из-за ссылки на лямбду), поэтому вы получаете ожидаемый ответ.
Закрытие над локальным временным значением на самом деле является стандартным подходом для захвата конкретного значения из какого-то момента времени внутри лямбды.
Ссылка Адама на блог Эрика Липперта дает более глубокое (и технически точное) описание происходящего.
Поскольку конструкция foreach
является просто синтаксическим сахаром, лучше всего думать об этом в его истинной форме.
int num;
while (nums.MoveNext())
{
num = nums.Current;
actions.Add(() => num);
}
Лямбда будет захватывать переменную num
, поэтому при выполнении лямбды будет использоваться последнее значение num
.
Это происходит по двум причинам:
1) делегаты сохраняют контекст (область действия) внешних переменных
2) первый цикл foreach будет компилироваться только за одно число объявлена переменная.
3) Ленивая оценка
Каждый делегированный, добавленный в первом цикле, сохранит одну и ту же переменную num, сохраненную в области видимости. Из-за ленивой оценки вы будете запускать делегатов после завершения первого цикла, поэтому число достоверных, сохраненных в области делегатов, равно 4.
См. запись в блоге Эрика Липперта по этому вопросу; она связана с тем, как переменные итератора скопированы в коде, и как это относится к лямбда-закрытиям и поднятым функциям.