Захваченная переменная в цикле в C #

Я встретил интересный вопрос о C #. У меня есть код, как показано ниже.

List<Func> actions = new List<Func>(); int variable = 0; while (variable  variable * 2); ++ variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Я ожидаю, что он выйдет 0, 2, 4, 6, 8. Однако на самом деле он выводит пять 10 секунд.

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

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

Да – возьмите копию переменной внутри цикла:

 while (variable < 5) { int copy = variable; actions.Add(() => copy * 2); ++ variable; } 

Вы можете думать об этом, как если бы компилятор C # создавал «новую» локальную переменную каждый раз, когда он попадает в объявление переменной. Фактически он создаст соответствующие новые объекты закрытия, и он становится сложным (с точки зрения реализации), если вы ссылаетесь на переменные в нескольких областях, но он работает 🙂

Обратите внимание, что более распространенным явлением этой проблемы является использование for или foreach :

 for (int i=0; i < 10; i++) // Just one variable foreach (string x in foo) // And again, despite how it reads out loud 

Подробнее об этом см. В разделе 7.14.4.2 спецификации C # 3.0, и в моей статье о закрытии также есть примеры.

Я считаю, что то, что вы переживаете, – это нечто вроде Closure http://en.wikipedia.org/wiki/Closure_(computer_science) . Ваша lamba имеет ссылку на переменную, которая находится вне самой функции. Ваш lamba не интерпретируется до тех пор, пока вы его не вызовете, и как только он получит значение, которое имеет переменная во время выполнения.

За кулисами компилятор генерирует class, который представляет собой замыкание для вызова метода. Он использует этот единственный экземпляр classа закрытия для каждой итерации цикла. Код выглядит примерно так, что облегчает понимание того, почему происходит ошибка:

 void Main() { List> actions = new List>(); int variable = 0; var closure = new CompilerGeneratedClosure(); Func anonymousMethodAction = null; while (closure.variable < 5) { if(anonymousMethodAction == null) anonymousMethodAction = new Func(closure.YourAnonymousMethod); //we're re-adding the same function actions.Add(anonymousMethodAction); ++closure.variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } } class CompilerGeneratedClosure { public int variable; public int YourAnonymousMethod() { return this.variable * 2; } } 

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

Для этого нужно сохранить значение, которое вам нужно в прокси-переменной, и получить эту переменную.

IE

 while( variable < 5 ) { int copy = variable; actions.Add( () => copy * 2 ); ++variable; } 

Да, вам нужно изменить variable в цикле и передать ее lambda таким образом:

 List> actions = new List>(); int variable = 0; while (variable < 5) { int variable1 = variable; actions.Add(() => variable1 * 2); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } Console.ReadLine(); 

Такая же ситуация происходит в многопоточном (C #, .NET 4.0).

См. Следующий код:

objective состоит в том, чтобы напечатать 1,2,3,4,5 по порядку.

 for (int counter = 1; counter <= 5; counter++) { new Thread (() => Console.Write (counter)).Start(); } 

Результат интересен! (Это может быть как 21334 …)

Единственное решение – использовать локальные переменные.

 for (int counter = 1; counter <= 5; counter++) { int localVar= counter; new Thread (() => Console.Write (localVar)).Start(); } 

Это не имеет никакого отношения к циклам.

Это поведение срабатывает, потому что вы используете выражение lambda () => variable * 2 где внешняя variable не определенная во внутренней области lambda.

Лямбда-выражения (в C # 3 +, а также анонимные методы в C # 2) по-прежнему создают реальные методы. Передача переменных этим методам связана с некоторыми дилеммами (передать по значению? Pass по ссылке? C # идет по ссылке – но это открывает еще одну проблему, когда ссылка может пережить реальную переменную). Что C # для решения всех этих дилемм заключается в создании нового вспомогательного classа («замыкание») с полями, соответствующими локальным переменным, используемым в lambda-выражениях, и методам, соответствующим фактическим lambda-методам. Любые изменения в variable в вашем коде фактически переводятся на изменение в ClosureClass.variable

Таким образом, ваш цикл while обновляет ClosureClass.variable до ClosureClass.variable пор, пока он не достигнет 10, тогда вы для циклов выполняете действия, которые все работают на одном и том же ClosureClass.variable .

Чтобы получить ожидаемый результат, вам необходимо создать разделение между переменной цикла и переменной, которая закрывается. Вы можете сделать это, введя другую переменную, то есть:

 List> actions = new List>(); int variable = 0; while (variable < 5) { var t = variable; // now t will be closured (ie replaced by a field in the new class) actions.Add(() => t * 2); ++variable; // changing variable won't affect the closured variable t } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Вы также можете переместить замыкание на другой метод для создания этого разделения:

 List> actions = new List>(); int variable = 0; while (variable < 5) { actions.Add(Mult(variable)); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Вы можете реализовать Mult в качестве lambda-выражения (неявное закрытие)

 static Func Mult(int i) { return () => i * 2; } 

или с фактическим вспомогательным classом:

 public class Helper { public int _i; public Helper(int i) { _i = i; } public int Method() { return _i * 2; } } static Func Mult(int i) { Helper help = new Helper(i); return help.Method; } 

В любом случае «Closures» не являются концепцией, связанной с циклами , а скорее с анонимными методами / lambda-выражениями, использующими локальные переменные с областью - хотя некоторые неосторожное использование циклов демонстрируют ловушки закрытия.

Давайте будем гением компьютера.