Acesso à variável foreach no aviso de fechamento

86

Estou recebendo o seguinte aviso:

Acesso para foreach variável no fechamento. Pode ter um comportamento diferente quando compilado com diferentes versões do compilador.

Isso é o que parece no meu editor:

mensagem de erro mencionada acima em um pop-up instantâneo

Eu sei como corrigir esse aviso, mas quero saber por que receberia este aviso?

É sobre a versão "CLR"? Está relacionado a "IL"?

Jeroen
fonte
1
Resposta TL; DR: adicione .ToList () ou .ToArray () no final de sua expressão de consulta e ele eliminará o aviso
JoelFan

Respostas:

136

Este aviso tem duas partes. O primeiro é ...

Acesso a foreach variável no fechamento

... o que não é inválido per se, mas é contra-intuitivo à primeira vista. Também é muito difícil fazer o certo. (Tanto é verdade que o artigo que link abaixo descreve isso como "prejudicial".)

Faça sua consulta, observando que o código que você extraiu é basicamente uma forma expandida do que o compilador C # (antes do C # 5) gera para foreach1 :

Eu [não] entendo porque [o seguinte] não é válido:

string s; while (enumerator.MoveNext()) { s = enumerator.Current; ...

Bem, é válido sintaticamente. E se tudo o que você está fazendo em seu loop for usar o valor de, sentão está tudo certo. Mas o fechamento slevará a um comportamento contra-intuitivo. Dê uma olhada no seguinte código:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

    while (enumerator.MoveNext())
    {
        s = enumerator.Current;

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

Se você executar este código, obterá a seguinte saída de console:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

Isso é o que você espera.

Para ver algo que você provavelmente não espera, execute o seguinte código imediatamente após o código acima:

foreach (var action in countingActions)
    action();

Você obterá a seguinte saída de console:

s == 5
s == 5
s == 5
s == 5
s == 5

Por quê? Porque criamos cinco funções que fazem exatamente a mesma coisa: imprimir o valor de s(que fechamos). Na realidade, têm a mesma função ("Imprimir s", "Imprimir s", "Imprimir s" ...).

No ponto em que vamos usá-los, eles fazem exatamente o que pedimos: imprima o valor de s. Se você olhar para o último valor conhecido de s, verá que é 5. Portanto, somos s == 5impressos cinco vezes no console.

Que é exatamente o que pedimos, mas provavelmente não é o que queremos.

A segunda parte do aviso ...

Pode ter comportamento diferente quando compilado com versões diferentes do compilador.

... é o que é. A partir do C # 5, o compilador gera um código diferente que "impede" que isso aconteça viaforeach .

Assim, o código a seguir produzirá resultados diferentes em diferentes versões do compilador:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

Consequentemente, também produzirá o aviso R # :)

Meu primeiro trecho de código, acima, exibirá o mesmo comportamento em todas as versões do compilador, já que não estou usando foreach(em vez disso, expandi-o da maneira que os compiladores pré-C # 5 fazem).

É para a versão CLR?

Não tenho certeza do que você está perguntando aqui.

A postagem de Eric Lippert diz que a mudança acontece "em C # 5". entãopresumivelmente, você deve direcionar o .NET 4.5 ou posterior com um compilador C # 5 ou posterior para obter o novo comportamento, e tudo antes disso obtém o comportamento antigo.

Mas, para ficar claro, é uma função do compilador e não da versão do .NET Framework.

Há relevância com IL?

Código diferente produz IL diferente, portanto, nesse sentido, há consequências para o IL gerado.

1 foreach é uma construção muito mais comum do que o código que você postou em seu comentário. O problema normalmente surge por meio do uso de foreach, não por meio da enumeração manual. É por isso que as alterações foreachno C # 5 ajudam a evitar esse problema, mas não completamente.

ta.speot.is
fonte
7
Na verdade, tentei o loop foreach em diferentes compiladores, obtendo resultados diferentes usando o mesmo destino (.Net 3.5). Eu usei VS2010 (que por sua vez usa o compilador associado ao .net 4.0 eu acredito) e VS2012 (compilador .net 4.5 eu acredito). Em princípio, isso significa que se você estiver usando o VS2013 e editando um projeto voltado para .Net 3.5 e compilando-o em um servidor de compilação que possui uma estrutura um pouco mais antiga instalada, você poderá ver resultados diferentes do seu programa em sua máquina em comparação com a compilação implantada.
Ykok de
Boa resposta, mas não tenho certeza de como "foreach" é relevante. Isso não aconteceria com a enumeração manual ou mesmo com um simples loop for (int i = 0; i <tamanho da coleção; i ++)? Parece ser um problema com os fechamentos que saem do escopo ou, mais precisamente, um problema com as pessoas que entendem como os fechamentos se comportam quando saem do escopo em que foram definidos.
Brad
O foreachmaterial aqui vem do conteúdo da pergunta. Você está certo ao dizer que isso pode acontecer de várias maneiras mais gerais.
ta.speot.is
1
Por que R # ainda me avisa, ele não lê a estrutura de destino, que eu configurei para 4.5.
Johnny_D
1
"Portanto, provavelmente você precisa direcionar o .NET 4.5 ou posterior" Esta afirmação não é verdadeira. A versão do .NET de destino não tem efeito sobre isso, o comportamento também é alterado no .NET 2.0, 3.5 e 4 se você estiver usando C # 5 (VS 2012 ou mais recente) para compilar. É por isso que você só recebe este aviso no .NET 4.0 ou anterior; se você direcionar o 4.5, não receberá o aviso porque não pode compilar o 4.5 em um compilador C # 4 ou anterior.
Scott Chamberlain
12

A primeira resposta é ótima, então pensei em apenas acrescentar uma coisa.

Você está recebendo o aviso porque, em seu código de exemplo, o modelo refletido está sendo atribuído a um IEnumerable, que só será avaliado no momento da enumeração, e a própria enumeração pode acontecer fora do loop se você atribuir o modelo refletido a algo com um escopo mais amplo .

Se você mudou

...Where(x => x.Name == property.Value)

para

...Where(x => x.Name == property.Value).ToList()

então, ReflectedModel seria atribuído a uma lista definida dentro do loop foreach, então você não receberia o aviso, já que a enumeração definitivamente aconteceria dentro do loop, e não fora dele.

David
fonte
Eu li um monte de explicações realmente longas que não resolveram esse problema para mim, então uma curta que resolveu. obrigado!
Charles Clayton
Eu li a resposta aceita e pensei "como é um encerramento se não está vinculando as variáveis?" mas agora eu entendo que é quando a avaliação acontece, obrigado!
Jerome
Sim, esta é uma solução universal óbvia. Lento, uso intensivo de memória, mas acho que realmente funciona 100% em todos os casos.
Al Kepp
8

Uma variável com escopo de bloco deve resolver o aviso.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Dmitry Gogol
fonte