Перебор списка действий

Я не могу понять, как пройтись по списку Action. Когда я пытаюсь это сделать, я получаю такие же значения, как и в предыдущей итерации.

Вот код (упрощенный пример):

string[] strings = { "abc", "def", "ghi" };

var actions = new List<Action>();
foreach (string str in strings)
    actions.Add(new Action(() => { Trace.WriteLine(str); }));

foreach (var action in actions)
    action();

Вывод:

ghi
ghi
ghi

Почему он всегда выбирает последний элемент в strings, когда выполняет действие?
И как я могу добиться желаемого результата, который будет выглядеть следующим образом:

abc
def
ghi

person demoncodemonkey    schedule 05.03.2012    source источник


Ответы (3)


Ваше действие является замыканием, поэтому оно обращается к самому str, а не к копии str:

foreach (string str in strings)
{
    var copy = str; // this will do the job
    actions.Add(new Action(() => { Trace.WriteLine(copy); }));
}
person Matthias Meid    schedule 05.03.2012
comment
Га, ты выиграл. Я знал, как это исправить, но не мог вспомнить, почему. Закрытие! Мне нужно закрытие! +1 :) - person Joshua; 05.03.2012
comment
@ Джошуа, это было не так давно, когда я узнал немного глубже :) ... это может быть полезно для дальнейшего чтения stackoverflow.com/questions/9412672/ - person Matthias Meid; 05.03.2012
comment
Интересно, никогда не понимал. Спасибо. - person demoncodemonkey; 05.03.2012

Это поведение обусловлено замыканиями.

Переменная, присутствующая в вашей лямбде, является ссылочной, а не копией значения. Это означает, что он указывает на последнее значение, принятое str, которое в вашем случае является «ghi». Вот почему для каждого вызова он просто переходит в одно и то же место в памяти и, естественно, восстанавливает одно и то же значение.

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

Кстати, если я не ошибаюсь, команда C# обещает исправить это неестественное поведение в C# 5.0. Так что лучше проверить их блог на эту тему для будущих обновлений.

person Tigran    schedule 05.03.2012
comment
+1 хорошее объяснение. Возможно, стоит упомянуть, что Java делает это по-другому. Если вы имеете дело с Runnable (обычно) для запуска потока, переменные копируются в новый контекст. Вот почему Java заставляет вас делать их final. - person Matthias Meid; 05.03.2012

Это довольно сложная ситуация. Короткий ответ — создать копию локальной переменной перед назначением ее замыканию:

string copy = str;
actions.Add(new Action(() => { Trace.WriteLine(copy); }));

Дополнительную информацию см. в этой статье о замыканиях.

person Bas    schedule 05.03.2012