Eu conheci uma questão interessante sobre c #. Eu tenho código como abaixo.
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => 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 isso se deve a todas as ações referentes a uma variável capturada. Como resultado, quando são chamados, todos 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?
c#
closures
captured-variable
Morgan Cheng
fonte
fonte
Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
.Respostas:
Sim - faça uma cópia da variável dentro do loop:
Você pode pensar nisso como se o compilador C # cria uma variável local "nova" toda vez que atinge a declaração da variável. De fato, ele criará novos objetos de fechamento apropriados e ficará complicado (em termos de implementação) se você se referir a variáveis em vários escopos, mas funciona :)
Observe que uma ocorrência mais comum desse problema está usando
for
ouforeach
:Consulte a seção 7.14.4.2 da especificação do C # 3.0 para obter mais detalhes, e meu artigo sobre encerramentos também tem mais exemplos.
Observe que, a partir do compilador C # 5 e além (mesmo ao especificar uma versão anterior do C #), o comportamento foi
foreach
alterado para que você não precise mais fazer uma cópia local. Veja esta resposta para mais detalhes.fonte
Acredito que você esteja enfrentando algo conhecido como Closure http://en.wikipedia.org/wiki/Closure_(computer_science) . Seu lamba tem uma referência a uma variável com escopo fora da própria função. Seu lamba não é interpretado até que você o invoque e, uma vez obtido, obterá o valor que a variável possui no tempo de execução.
fonte
Nos bastidores, o compilador está gerando uma classe que representa o fechamento da sua chamada de método. Ele usa essa instância única da classe de fechamento para cada iteração do loop. O código se parece com isso, o que facilita a compreensão do motivo pelo qual o bug ocorre:
Na verdade, esse não é o código compilado do seu exemplo, mas examinei meu próprio código e parece muito com o que o compilador realmente geraria.
fonte
A maneira de contornar isso é armazenar o valor necessário em uma variável proxy e fazer com que essa variável seja capturada.
IE
fonte
Isso não tem nada a ver com loops.
Esse comportamento é acionado porque você usa uma expressão lambda em
() => variable * 2
que o escopo externovariable
não é realmente definido no escopo interno do lambda.Expressões lambda (em C # 3 +, bem como métodos anônimos em C # 2) ainda criam métodos reais. A passagem de variáveis para esses métodos envolve alguns dilemas (passar por valor? Passar por referência? C # é seguido 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 classe auxiliar ("fechamento") com campos correspondentes às variáveis locais usadas nas expressões lambda e métodos correspondentes aos métodos lambda reais. Quaisquer alterações
variable
no seu código são realmente traduzidas para alterações nesse código.ClosureClass.variable
Portanto, seu loop while continua atualizando o
ClosureClass.variable
até chegar a 10, e você executa loops para todos os loopsClosureClass.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, ou seja:
Você também pode mover o fechamento para outro método para criar esta separação:
Você pode implementar Mult como uma expressão lambda (fechamento implícito)
ou com uma classe auxiliar real:
De qualquer forma, "Closures" NÃO é um conceito relacionado a loops , mas sim a métodos anônimos / expressões lambda, uso de variáveis de escopo local - embora algum uso incauto de loops demonstre armadilhas de fechamento.
fonte
Sim, você precisa escopo
variable
dentro do loop e passá-lo para o lambda dessa maneira:fonte
A mesma situação está acontecendo no multithread (C #, .NET 4.0].
Veja o seguinte código:
O objetivo é imprimir 1,2,3,4,5 em ordem.
A saída é interessante! (Pode ser como 21334 ...)
A única solução é usar variáveis locais.
fonte
Como ninguém aqui citou diretamente o ECMA-334 :
Mais adiante na especificação,
Sim, acho que deve ser mencionado que em C ++ esse problema não ocorre porque você pode escolher se a variável é capturada por valor ou por referência (consulte: Captura Lambda ).
fonte
É chamado de problema de fechamento, basta usar uma variável de cópia e pronto.
fonte