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

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

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

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

Приведёт к серии "Ошибка: x", где большая часть x равна 50.

Аналогично:

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

В результате "Использование значения: после".

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

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

Ответ 1

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

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

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);
}

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

  • второй фрагмент создаст 50 закрытий и 50 делегатов
  • третий фрагмент создаст 50 коробочных int, но только один делегат

Ответ 2

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

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

Ответ 3

Лямбда-выражения захватывают не значение внешней переменной, а ссылку на нее. Вот почему вы видите 50 или After в своих задачах.

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

Это неудачное поведение будет исправлено компилятором С# с .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());

Ответ 4

Лямбда-выражения по определению лениво оцениваются, поэтому они не будут оцениваться до фактического вызова. В вашем случае выполнение задачи. Если вы закроете локальное выражение в выражении лямбда, будет отражено состояние локального на момент выполнения. Это то, что вы видите. Вы можете воспользоваться этим. Например. ваш цикл 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 во время выполнения Action, и состояние будет часто состоять из состояния после завершения цикла. В последнем случае 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);
}