Как я могу зафиксировать значение внешней переменной внутри лямбда-выражения?
Я только что столкнулся со следующим поведением:
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();
4 ответа
Это больше связано с лямбдами, чем с потоками. Лямбда захватывает ссылку на переменную, а не ее значение. Это означает, что когда вы пытаетесь использовать i в своем коде, его значение будет тем, что было сохранено в i последним.
Чтобы избежать этого, вы должны скопировать значение переменной в локальную переменную при запуске лямбды. Проблема в том, что запуск задачи сопряжен с издержками, и первая копия может быть выполнена только после завершения цикла. Следующий код также потерпит неудачу
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 штучных целых, но только один делегат
Это потому, что вы запускаете код в новом потоке, и основной поток немедленно переходит к изменению переменной. Если бы лямбда-выражение было выполнено немедленно, весь смысл использования задачи был бы потерян.
Поток не получает свою собственную копию переменной во время создания задачи, все задачи используют одну и ту же переменную (которая на самом деле хранится в замыкании для метода, это не локальная переменная).
Лямбда-выражения фиксируют не значение внешней переменной, а ссылку на нее. Вот причина, почему вы видите 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());
Лямбда-выражения по определению лениво оцениваются, поэтому они не будут оцениваться до тех пор, пока не будут вызваны. В вашем случае выполнением задачи. Если в вашем лямбда-выражении вы закроете локальный элемент, будет отражено состояние локального элемента на момент выполнения. Что вы видите. Вы можете воспользоваться этим. Например, вашему циклу 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);
}