Использование переменной итератора цикла foreach в выражении лямбда - почему не удается?
Рассмотрим следующий код:
public class MyClass
{
public delegate string PrintHelloType(string greeting);
public void Execute()
{
Type[] types = new Type[] { typeof(string), typeof(float), typeof(int)};
List<PrintHelloType> helloMethods = new List<PrintHelloType>();
foreach (var type in types)
{
var sayHello =
new PrintHelloType(greeting => SayGreetingToType(type, greeting));
helloMethods.Add(sayHello);
}
foreach (var helloMethod in helloMethods)
{
Console.WriteLine(helloMethod("Hi"));
}
}
public string SayGreetingToType(Type type, string greetingText)
{
return greetingText + " " + type.Name;
}
...
}
После вызова myClass.Execute()
код печатает следующий неожиданный ответ:
Hi Int32
Hi Int32
Hi Int32
Очевидно, я ожидал бы "Hi String"
, "Hi Single"
, "Hi Int32"
, но, по-видимому, это не так. Почему последний элемент итерационного массива используется во всех трех методах вместо подходящего?
Как бы вы переписали код для достижения желаемой цели?
Ответы
Ответ 1
Добро пожаловать в мир замыканий и захваченных переменных:)
Эрик Липперт имеет подробное объяснение этого поведения:
в основном, это переменная цикла, которая фиксируется, а не значение.
Чтобы получить то, что, по вашему мнению, вам нужно получить, сделайте следующее:
foreach (var type in types)
{
var newType = type;
var sayHello =
new PrintHelloType(greeting => SayGreetingToType(newType, greeting));
helloMethods.Add(sayHello);
}
Ответ 2
В качестве краткого объяснения, которое ссылается на сообщения блога, на которые ссылается SWeko, лямбда захватывает переменную, а не значение. В цикле foreach переменная не является уникальной на каждой итерации, та же переменная используется для продолжительности цикла (это более очевидно, когда вы видите расширение, которое компилятор выполняет в foreach во время компиляции). В результате вы захватили одну и ту же переменную во время каждой итерации, а переменная (как и последняя итерация) относится к последнему элементу вашего набора.
Обновление: В новых версиях языка (начиная с С# 5) переменная цикла считается новой с каждой итерацией, поэтому закрытие ее не приводит к той же проблеме, что и в более ранних версиях версии (С# 4 и ранее).
Ответ 3
Вы можете исправить это, введя дополнительную переменную:
...
foreach (var type in types)
{
var t = type;
var sayHello = new PrintHelloType(greeting => SayGreetingToType(t, greeting));
helloMethods.Add(sayHello);
}
....