У меня есть отрывок кода, что я думал, будет работать из-за закрытий; однако, результат доказывает иначе. Что продолжается здесь, чтобы это не произвело ожидаемый вывод (одно из каждого слова)?
Код:
string[] source = new string[] {"this", "that", "other"};
List<Thread> testThreads = new List<Thread>();
foreach (string text in source)
{
testThreads.Add(new Thread(() =>
{
Console.WriteLine(text);
}));
}
testThreads.ForEach(t => t.Start())
Вывод:
other
other
other
Это связано с тем, что замыкания захватывают саму переменную, не оценивая ее до тех пор, пока она не будет фактически использована. После завершения цикла foreach значение text
является «другим», и, как и после завершения цикла, вызывается метод, а во время вызова значение захваченной переменной text
is "other"
Подробности см. в этом сообщении в блоге Эрика Липперта . Он объясняет поведение и некоторые его причины.
Причина, по которой это происходит, заключается в том, что к тому моменту, когда вы запускаете свои потоки, цикл завершается, и значение текстовой локальной переменной - «другое», поэтому, когда вы запускаете потоки, это то, что печатается. Это можно легко исправить:
string[] source = new string[] {"this", "that", "other"};
foreach (string text in source)
{
new Thread(t => Console.WriteLine(t)).Start(text);
}
Это классическая ошибка захвата переменной цикла. Это влияет как на for
, так и на foreach
циклы: предполагая типичную конструкцию, у вас есть одна переменная на протяжении всей продолжительности цикла. Когда переменная захватывается лямбда-выражением или анонимным методом, фиксируется сама переменная (а не значение во время захвата). Если вы измените значение переменной, а затем выполните делегат, делегат «увидит» это изменение.
Эрик Липперт подробно описывает это в своем блоге: часть 1 , часть 2 .
Обычное решение - взять копию переменной внутри цикла :
string[] source = new string[] {"this", "that", "other"};
List<Thread> testThreads = new List<Thread>();
foreach (string text in source)
{
string copy = text;
testThreads.Add(new Thread(() =>
{
Console.WriteLine(copy);
}));
}
testThreads.ForEach(t => t.Start())
Причина, по которой это работает, заключается в том, что каждый делегат теперь будет захватывать другой «экземпляр» копии
переменная. Захваченная переменная будет создана для итерации цикла, которой для этой итерации присвоено значение text
. И вот, все работает.
Замыкания в C # не фиксируют значение текста во время создания. Поскольку цикл foreach завершает выполнение до выполнения любого из потоков, каждому дается последнее значение text
.
Это можно исправить:
string[] source = new string[] {"this", "that", "other"};
List<Thread> testThreads = new List<Thread>();
foreach (string text in source)
{
// Capture the text before using it in a closure
string capturedText = text;
testThreads.Add(new Thread(() =>
{
Console.WriteLine(capturedText);
}));
}
testThreads.ForEach(t => t.Start());
Как видите, этот код «захватывает» значение text
внутри каждой итерации цикла for. Это гарантирует, что замыкание получит уникальную ссылку для каждой итерации, а не будет использовать одну и ту же ссылку в конце.
Другие объяснили, почему вы столкнулись с этой проблемой.
К счастью, исправить очень просто:
foreach (string text in source)
{
string textLocal = text; // this is all you need to add
testThreads.Add(new Thread(() =>
{
Console.WriteLine(textLocal); // well, and change this line
}));
}
Замыкания / лямбда-выражения не могут правильно связываться с переменными foreach или счетчиками цикла. Скопируйте значение в другую локальную переменную (не объявленную как переменную foreach или счетчик), и она будет работать должным образом.