Variável capturada em um loop em c #

Eu encontrei uma questão interessante sobre o C #. Eu tenho código como abaixo.

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

Espero que ele produza 0, 2, 4, 6, 8. No entanto, ele realmente produz cinco 10s.

Parece que é devido a todas as ações referentes a uma variável capturada. Como resultado, quando são invocados, todos eles têm a mesma saída.

Existe uma maneira de contornar esse limite para que cada instância de ação tenha sua própria variável capturada?

    Sim – tire uma cópia da variável dentro do loop:

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

    Você pode pensar nisso como se o compilador C # criasse uma variável local “nova” sempre que atingisse a declaração de variável. Na verdade, ele criará novos objects de fechamento apropriados e ficará complicado (em termos de implementação) se você se referir a variables ​​em vários escopos, mas funciona 🙂

    Observe que uma ocorrência mais comum desse problema é usar for ou foreach :

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

    Consulte a seção 7.14.4.2 da especificação do C # 3.0 para obter mais detalhes sobre isso, e meu artigo sobre fechamento também tem mais exemplos.

    Eu acredito que o que você está experimentando é algo conhecido como Encerramento http://en.wikipedia.org/wiki/Closure_(computer_science) . Seu lamba tem uma referência a uma variável que está fora do escopo da própria function. Seu lamba não é interpretado até que você o invoque e, uma vez que esteja, ele obterá o valor que a variável possui no tempo de execução.

    Nos bastidores, o compilador está gerando uma class que representa o fechamento para sua chamada de método. Ele usa essa única instância da class de fechamento para cada iteração do loop. O código se parece com algo assim, o que torna mais fácil entender por que o bug acontece:

     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; } } 

    Este não é realmente o código compilado do seu exemplo, mas eu examinei o meu próprio código e isso se parece muito com o que o compilador realmente geraria.

    A maneira de contornar isso é armazenar o valor que você precisa em uma variável proxy e ter essa variável capturada.

    IE

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

    Sim, você precisa escopo variable dentro do loop e passá-lo para o lambda dessa maneira:

     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(); 

    A mesma situação está acontecendo em multi-threading (C #, .NET 4.0].

    Veja o seguinte código:

    O objective é imprimir 1,2,3,4,5 em ordem.

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

    A saída é interessante! (Pode ser como 21334 …)

    A única solução é usar variables ​​locais.

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

    Isso não tem nada a ver com loops.

    Esse comportamento é acionado porque você usa uma expressão lambda () => variable * 2 que a variable escopo externo não é realmente definida no escopo interno do lambda.

    Expressões lambda (em C # 3 +, assim como methods anônimos em C # 2) ainda criam methods reais. Passar variables ​​para esses methods envolve alguns dilemas (passar por valor? Passar por referência? C # vai por referência – mas isso abre outro problema em que a referência pode sobreviver à variável real). O que o C # faz para resolver todos esses dilemas é criar uma nova class auxiliar (“closure”) com campos correspondentes às variables ​​locais usadas nas expressões lambda e methods correspondentes aos methods lambda reais. Quaisquer alterações na variable em seu código são realmente traduzidas para alterar essa ClosureClass.variable

    Então o seu loop while mantém atualizando o ClosureClass.variable até que ele atinja 10, então você for loops executa as ações, todas operando na mesma ClosureClass.variable .

    Para obter o resultado esperado, você precisa criar uma separação entre a variável de loop e a variável que está sendo fechada. Você pode fazer isso introduzindo outra variável, por exemplo:

     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()); } 

    Você também pode mover o fechamento para outro método para criar essa separação:

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

    Você pode implementar Mult como uma expressão lambda (fechamento implícito)

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

    ou com uma class auxiliar real:

     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; } 

    Em qualquer caso, "Closures" NÃO são um conceito relacionado a loops , mas sim a methods anônimos / expressões lambda usando variables ​​de escopo local - embora alguns usos imprudentes de loops demonstrem traps de encerramento.