Проблема лямбда-захвата с итераторами? - PullRequest
2 голосов
/ 19 августа 2011

Извиняюсь, если этот вопрос уже задавался, но предположим, что у нас есть этот код (я запустил его с Mono 2.10.2 и скомпилировал с gmcs 2.10.2.0):

using System;

public class App {
    public static void Main(string[] args) {
        Func<string> f = null;
        var strs = new string[]{
            "foo",
            "bar",
            "zar"
        };

        foreach (var str in strs) {
            if ("foo".Equals(str)) 
                f = () => str;
        }
        Console.WriteLine(f());     // [1]: Prints 'zar'

        foreach (var str in strs) {
            var localStr = str;
            if ("foo".Equals(str))
                f = () => localStr;
        }
        Console.WriteLine(f());     // [2]: Prints 'foo'

        { int i = 0;
        for (string str; i < strs.Length; ++i) {
            str = strs[i];
            if ("foo".Equals(str)) 
                f = () => str;
        }}
        Console.WriteLine(f());     // [3]: Prints 'zar'
    }
}

Кажется логичным, что [1] печатает так же, как [3]. Но, честно говоря, я как-то ожидал, что он напечатает так же, как [2]. Я почему-то верил, что реализация [1] будет ближе к [2].

Вопрос : Может ли кто-нибудь предоставить ссылку на спецификацию, где она точно сообщает, как переменная str (или, возможно, даже итератор) фиксируется лямбда-выражением в [1].

Полагаю, мне нужна точная реализация цикла foreach.

Ответы [ 4 ]

11 голосов
/ 19 августа 2011

Вы просили ссылку на спецификацию;соответствующее расположение - раздел 8.8.4, в котором говорится, что цикл «foreach» эквивалентен:

    V v;
    while (e.MoveNext()) {
        v = (V)(T)e.Current;
        embedded-statement
    }

Обратите внимание, что значение v объявлено вне цикла while, и поэтому существует одна переменная цикла,Затем это закрывается лямбда-выражением.

ОБНОВЛЕНИЕ

Поскольку многие сталкиваются с этой проблемой, команда разработчиков и проектировщиков C # изменила C # 5, чтобы иметь эту семантику :

    while (e.MoveNext()) {
        V v = (V)(T)e.Current;
        embedded-statement
    }

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

Помните, что C # 2, 3 и 4 теперь несовместимы с C # 5 в этом отношении.Также обратите внимание, что изменение относится только к foreach, а не к for петлям.

Подробнее см. http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/.


Состояния абергмейера Commenter:

C # - единственный язык с таким странным поведением.

Это утверждение категорически неверно .Рассмотрим следующий код JavaScript:

var funcs = [];
var results = [];
for(prop in { a : 10, b : 20 })
{
  funcs.push(function() { return prop; });
  results.push(funcs[0]());
}

abergmeier, не могли бы вы догадаться, каково содержание results?

2 голосов
/ 19 августа 2011

Основным различием между 1/3 и 2 является время жизни перехватываемой переменной. В 1 и 3 лямбда захватывает переменную итерации str. В циклах for и foreach есть одна переменная итерации для времени жизни цикла. Когда лямбда выполняется в конце цикла, она выполняется с окончательным значением: zar

В 2 вы захватываете локальную переменную, время жизни которой является одной итерацией цикла. Следовательно, вы фиксируете значение в это время "foo"

Лучшее упоминание, которое я могу вам дать, это сообщение Эрика в блоге на эту тему

0 голосов
/ 26 декабря 2014

Для людей из Google

Я исправил лямбда-ошибку, используя этот подход:

Я изменил это

for(int i=0;i<9;i++)
    btn.OnTap += () => { ChangeCurField(i * 2); };

к этому

for(int i=0;i<9;i++)
{
    int numb = i * 2;
    btn.OnTap += () => { ChangeCurField(numb); };
}

Это заставляет переменную "numb" быть единственной для лямбды, а также генерирует генерирование в этот момент, а не при вызове / генерировании лямбды <не уверен, когда это произойдет.

0 голосов
/ 19 августа 2011

В контуре 1 и 3 происходит следующее:

Текущее значение присваивается переменной str. Это всегда одна и та же переменная, просто с разным значением в каждой итерации. Эта переменная фиксируется лямбда-выражением. Поскольку лямбда выполняется после завершения цикла, она имеет значение последнего элемента в вашем массиве.

В цикле 2 происходит следующее:

Текущее значение назначено новой переменной localStr. Это всегда новая переменная, которая получает назначенное значение. Эта новая переменная фиксируется лямбда-выражением. Поскольку следующая итерация цикла создает новую переменную, значение захваченной переменной не изменяется, и поэтому она выводит «foo».

...