Как делает лямбду в C#, связывают с перечислителем в 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 'цифра' на самом деле связывается с 'Текущим' вместо результата, возвращенного им?

13
задан scobi 10 March 2010 в 00:35
поделиться

5 ответов

Это хорошо известное и устоявшееся поведение ламбд, хотя часто удивляет тех, кто сталкивается с ним впервые. Основная проблема заключается в том, что ваша мысленная модель того, что такое лямбда это , не совсем верна.

Лямбда - это функция, которая не выполняется, пока ее не вызовут. Ваше закрытие связывает ссылку на экземпляр лямбды, а не значение. Когда вы выполняете свои действия в последнем цикле foreach, вы в первый раз на самом деле следуете за закрытой ссылкой, чтобы увидеть, что это такое.

В первом случае вы ссылаетесь на num, и в этот момент значение num равно 4, поэтому, конечно, все ваши выходные данные равны 4. Во втором случае каждая лямбда была привязана к другому значению, которое каждый раз было локальным для цикла, и это значение не изменилось (оно не было GC'd только из-за ссылки на лямбду), поэтому вы получаете ожидаемый ответ.

Закрытие над локальным временным значением на самом деле является стандартным подходом для захвата конкретного значения из какого-то момента времени внутри лямбды.

Ссылка Адама на блог Эрика Липперта дает более глубокое (и технически точное) описание происходящего.

7
ответ дан 2 December 2019 в 00:31
поделиться

Поскольку конструкция foreach является просто синтаксическим сахаром, лучше всего думать об этом в его истинной форме.

int num;
while (nums.MoveNext())
{
    num = nums.Current;
    actions.Add(() => num);
}

Лямбда будет захватывать переменную num , поэтому при выполнении лямбды будет использоваться последнее значение num .

{ {1}}
2
ответ дан 2 December 2019 в 00:31
поделиться

Это происходит по двум причинам:
1) делегаты сохраняют контекст (область действия) внешних переменных
2) первый цикл foreach будет компилироваться только за одно число объявлена ​​переменная.
3) Ленивая оценка

Каждый делегированный, добавленный в первом цикле, сохранит одну и ту же переменную num, сохраненную в области видимости. Из-за ленивой оценки вы будете запускать делегатов после завершения первого цикла, поэтому число достоверных, сохраненных в области делегатов, равно 4.

1
ответ дан 2 December 2019 в 00:31
поделиться

См. запись в блоге Эрика Липперта по этому вопросу; она связана с тем, как переменные итератора скопированы в коде, и как это относится к лямбда-закрытиям и поднятым функциям.

3
ответ дан 2 December 2019 в 00:31
поделиться
Другие вопросы по тегам:

Похожие вопросы: