Как лямбда в C # связывается с перечислителем в foreach? - PullRequest
13 голосов
/ 10 марта 2010

Я только что столкнулся с самым неожиданным поведением. Я уверен, что есть веская причина, по которой это работает. Может кто-нибудь помочь объяснить это?

Рассмотрим этот код:

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', а не с результатом, возвращаемым им?

Ответы [ 5 ]

7 голосов
/ 10 марта 2010

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

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

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

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

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

3 голосов
/ 10 марта 2010

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

2 голосов
/ 10 марта 2010

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

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

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

1 голос
/ 11 марта 2010
1 голос
/ 10 марта 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...