Как я могу получить значение внешней переменной внутри лямбда-выражения?

Я только что столкнулся со следующим поведением:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i.ToString());
    });
}

Результатом будет серия «Ошибка: х», где большинство х равно 50.

Так же:

var a = "Before";
var task = new Task(() => Debug.Print("Using value: " + a));
a = "After";
task.Start();

Результатом будет & quot; Использование значения: после & quot ;.

Это ясно означает, что конкатенация в лямбда-выражении происходит не сразу. Как можно использовать копию внешней переменной в лямбда-выражении во время объявления выражения? Следующее не будет работать лучше (что не обязательно противоречиво, я признаю):

var a = "Before";
var task = new Task(() => {
    var a2 = a;
    Debug.Print("Using value: " + a2);
});
a = "After";
task.Start();
 Erwin Mayer15 июн. 2012 г., 13:27
Правда, Джеймс, на самом деле, после прочтения первоначальных комментариев я изменил название своего вопроса, чтобы лучше отразить, о чем я говорил
 James Manning15 июн. 2012 г., 13:17
ИМХО, вы заканчиваете тем, что задаете здесь 2 вопроса - «реальный» один из них, по-видимому, находится в заголовке (как захватить значение, чтобы задача выполнялась на значении во время цикла), но тогда, по-видимому, основная часть вопроса сосредоточена на «почему эти вещи приводят к неожиданным значениям»; (эффект захвата замыкания означает, что все они ссылаются на одну и ту же переменную). Таким образом, в результате вы получите большинство ответов, объясняющих поведение, а не ответы на ваши «реальные» вопросы. вопрос (АФАКТ :)
 Panagiotis Kanavos15 июн. 2012 г., 12:58
Возможная копия "Перехваченная переменная C # в цикле"stackoverflow.com/questions/271440/…
 Vlad15 июн. 2012 г., 12:58
Зачем им? Они асинхронные в любом случае.

Ответы на вопрос(4)

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

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

Решение Вопроса

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

Чтобы избежать этого, вы должны скопировать значение переменной в локальную переменную при запуске лямбды. Проблема в том, что запуск задачи сопряжен с издержками, и первая копия может быть выполнена только после завершения цикла.The following code will also fail

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(() => {
        var i1=i;
        Debug.Print("Error: " + i1.ToString());
    });
}

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

for (var i = 0; i < 50; ++i) {
    var i1=i;
    Task.Factory.StartNew(() => {
        Debug.Print("Error: " + i1.ToString());
    });
}

Лучшее решение - передать параметр цикла в качестве параметра состояния:

for (var i = 0; i < 50; ++i) {
    Task.Factory.StartNew(o => {
        var i1=(int)o;
        Debug.Print("Error: " + i1.ToString());
    }, i);
}

Использование параметра состояния приводит к меньшему количеству выделений. Глядя на декомпилированный код:

the second snippet will create 50 closures and 50 delegates the third snippet will create 50 boxed ints but only a single delegate
 15 июн. 2012 г., 13:19
Для первой петли «право» исправить (AFAICT) - сделать var i1 = i; внутри цикла, но перед Task.Factory.StartNew. С этим изменением каждое замыкание будет ссылаться на свою отдельную переменную, и вы получите правильный эффект. Параметр состояния позволяет избежать необходимости закрытия, поэтому, безусловно, более эффективен, но не обязателен, если вы просто хотите правильного поведения.
 15 июн. 2012 г., 13:19
Дело не в том, что он не работает (так работает язык), а в том, что лямбды могут начинать выполнение только ПОСЛЕ окончания цикла
 15 июн. 2012 г., 13:16
Ситуация достаточно известна, она описана здесь:blogs.msdn.com/b/ericlippert/archive/2009/11/12/…
 15 июн. 2012 г., 13:20
@ Джеймс Мэннинг, вы правы, это создает переменную, локальную только для цикла, поэтому нет шансов перехватить неправильную переменную
 Erwin Mayer15 июн. 2012 г., 13:17
Вторая часть работает, но не первая, поэтому я пока не могу отметить ее как принятое решение.

а ссылку на нее. Вот почему вы видите50 или жеAfter в ваших задачах.

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

Это неудачное поведение будет исправлено компилятором C # с .NET 4.5 до тех пор, пока вам не придется жить с этой странностью.

Пример:

    List<Action> acc = new List<Action>();
    for (int i = 0; i < 10; i++)
    {
        int tmp = i;
        acc.Add(() => { Console.WriteLine(tmp); });
    }

    acc.ForEach(x => x());
 Erwin Mayer15 июн. 2012 г., 13:07
Вы имеете в виду создание копии в лямбда-выражении будет работать? В настоящее время это не так: Использование var a2 = a; Logging.Print (& quot; Использование значения: & quot; + a2); по-прежнему повторяет & quot; Использование значения: после & quot ;.
 15 июн. 2012 г., 13:12
Сожалею. Вам нужно разместить копию вне лямбды, чтобы она работала.

поэтому они не будут оцениваться до тех пор, пока не будут вызваны. В вашем случае выполнением задачи. Если вы закроете локальное в своем лямбда-выражении, состояние локального на момент выполнения будет отражено. Что вы видите. Вы можете воспользоваться этим. Например. Ваш цикл for действительно не требует новой лямбды для каждой итерации, предполагая в качестве примера, что описанный результат был тем, что вы намеревались написать

var i =0;
Action<int> action = () => Debug.Print("Error: " + i);
for(;i<50;+i){
    Task.Factory.StartNew(action);
}

с другой стороны, если вы хотите, чтобы это на самом деле напечатано"Error: 1"..."Error 50" Вы могли бы изменить выше, чтобы

var i =0;
Func<Action<int>> action = (x) => { return () => Debug.Print("Error: " + x);}
for(;i<50;+i){
    Task.Factory.StartNew(action(i));
}

Первый закрываетсяi и будет использовать состояниеi в то время, когда Действие выполняется, и состояние часто становится состоянием после завершения цикла. В последнем случаеi оценивается с нетерпением, потому что он передается в качестве аргумента функции. Эта функция затем возвращаетAction<int> который передаетсяStartNew.

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

for (var i = 0; i < 50; ++i) {
    var j = i;
    Task.Factory.StartNew(() => Debug.Print("Error: " + j));
}

Все вышесказанное является общим для лямбд. В конкретном случаеStartNew фактически существует перегрузка, которая делает то, что делает второй пример, так что ее можно упростить до

var i =0;
Action<object> action = (x) => Debug.Print("Error: " + x);}
for(;i<50;+i){
    Task.Factory.StartNew(action,i);
}
 25 февр. 2013 г., 14:16
Кстати, лямбда может быть упрощена доx => () => Debug.Print("Error: " + x).

Ваш ответ на вопрос