Как я могу захватить значение внешней переменной внутри выражения лямбда?
Я просто столкнулся с следующим поведением:
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();
Ответы
Ответ 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);
}