Existe uma razão para a reutilização da variável em C # em um foreach?

1685

Ao usar expressões lambda ou métodos anônimos em C #, precisamos ter cuidado com o acesso à armadilha de fechamento modificada . Por exemplo:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

Devido ao fechamento modificado, o código acima fará com que todas as Wherecláusulas da consulta sejam baseadas no valor final des .

Como explicado aqui , isso acontece porque a svariável declarada no foreachloop acima é traduzida assim no compilador:

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

em vez de assim:

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

Conforme indicado aqui , não há vantagens de desempenho em declarar uma variável fora do loop e, em circunstâncias normais, a única razão pela qual posso pensar nisso é se você planeja usar a variável fora do escopo do loop:

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

No entanto, variáveis ​​definidas em um foreachloop não podem ser usadas fora do loop:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Portanto, o compilador declara a variável de uma maneira que a torna altamente propensa a um erro que geralmente é difícil de encontrar e depurar, sem produzir benefícios perceptíveis.

Existe algo que você pode fazer com foreachloops dessa maneira que não seria possível se eles fossem compilados com uma variável com escopo interno ou essa é apenas uma escolha arbitrária que foi feita antes que métodos anônimos e expressões lambda estivessem disponíveis ou comuns e que não foi revisado desde então?

StriplingWarrior
fonte
4
O que há de errado String s; foreach (s in strings) { ... }?
Brad Christie
5
@BradChristie o OP não está realmente a falar foreach, mas sobre expressões Lamda, resultando em código semelhante como mostrado pelo OP ...
Yahia
22
@BradChristie: Isso compila? ( Erro: Tipo e identificador são ambos necessários em uma declaração foreach para mim)
Austin Salonen
32
@JakobBotschNielsen: é um local externo fechado de uma lambda; por que você está assumindo que ele estará na pilha? Sua vida útil é mais longa que o quadro da pilha !
precisa saber é o seguinte
3
@ EricLippert: Estou confuso. Entendo que o lambda captura uma referência à variável foreach (que é declarada internamente fora do loop) e, portanto, você acaba comparando com seu valor final; que eu recebo. O que não entendo é como declarar a variável dentro do loop fará alguma diferença. Do ponto de vista do compilador-gravador, estou alocando apenas uma referência de string (var 's') na pilha, independentemente de a declaração estar dentro ou fora do loop; Eu certamente não gostaria de inserir uma nova referência na pilha a cada iteração!
Anthony

Respostas:

1407

O compilador declara a variável de uma maneira que a torna altamente propensa a um erro que geralmente é difícil de encontrar e depurar, sem produzir benefícios perceptíveis.

Sua crítica é inteiramente justificada.

Discuto esse problema em detalhes aqui:

Fechar sobre a variável do loop considerada prejudicial

Existe algo que você possa fazer com os loops foreach dessa maneira que não seria possível se eles fossem compilados com uma variável com escopo interno? ou isso é apenas uma escolha arbitrária que foi feita antes que métodos anônimos e expressões lambda estivessem disponíveis ou comuns e que não foram revisados ​​desde então?

O último. A especificação C # 1.0, na verdade, não dizia se a variável do loop estava dentro ou fora do corpo do loop, pois não fazia diferença observável. Quando a semântica de fechamento foi introduzida no C # 2.0, optou-se por colocar a variável do loop fora do loop, consistente com o loop "for".

Eu acho que é justo dizer que todos lamentam essa decisão. Essa é uma das piores "pegadinhas" do C #, e vamos fazer a alteração final para corrigi-la. No C # 5, a variável de loop foreach estará logicamente dentro do corpo do loop e, portanto, os fechamentos receberão uma cópia nova sempre.

O forloop não será alterado e a alteração não será "portada para trás" para versões anteriores do C #. Portanto, você deve continuar tomando cuidado ao usar esse idioma.

Eric Lippert
fonte
177
De fato, refutamos essa alteração no C # 3 e C # 4. Quando projetamos o C # 3, percebemos que o problema (que já existia no C # 2) pioraria porque haveria muitas lambdas (e consultas compreensões, que são lambdas disfarçadas) em loops foreach graças ao LINQ. Lamento que esperávamos que o problema seja suficientemente mau mandado de corrigi-lo tão tarde, em vez de corrigi-lo em C # 3.
Eric Lippert
75
E agora teremos que lembrar que foreaché "seguro", mas fornão é.
Leppie
22
@michielvoo: A mudança está ocorrendo no sentido de que não é compatível com versões anteriores. O novo código não será executado corretamente ao usar um compilador mais antigo.
Leppie
41
@Benjol: Não, é por isso que estamos dispostos a aceitá-lo. Jon Skeet apontou para mim um cenário importante de mudança de quebra, que é que alguém escreve código em C # 5, o testa e o compartilha com pessoas que ainda usam C # 4, que ingenuamente acreditam que está correto. Espero que o número de pessoas afetadas por esse cenário seja pequeno.
Eric Lippert
29
Como um aparte, o ReSharper sempre captou isso e o reporta como "acesso ao fechamento modificado". Então, pressionando Alt + Enter, ele corrigirá automaticamente seu código para você. jetbrains.com/resharper
Mike Chamberlain
191

O que você está perguntando é totalmente abordado por Eric Lippert em seu post no blog, Closing over the loop, a variável considerada prejudicial e sua sequência.

Para mim, o argumento mais convincente é que ter nova variável em cada iteração seria inconsistente com o for(;;)loop de estilo. Você esperaria ter um novo int iem cada iteração defor (int i = 0; i < 10; i++) ?

O problema mais comum com esse comportamento é fechar a variável de iteração e possui uma solução fácil:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Minha postagem no blog sobre esse problema: Encerramento sobre a variável foreach em C # .

Krizz
fonte
18
Por fim, o que as pessoas realmente querem quando escrevem isso não é ter várias variáveis, é fechar o valor . E é difícil pensar em uma sintaxe utilizável para isso no caso geral.
precisa saber é o seguinte
1
Sim, não é possível fechar pelo valor, no entanto, há uma solução muito fácil que acabei de editar minha resposta para incluir.
Krizz
6
É um fechamento muito ruim em C # perto de referências. Se eles fecharam os valores por padrão, poderíamos especificar facilmente o fechamento das variáveis ​​em vez de ref.
Sean U
2
@Krizz, este é um caso em que a consistência forçada é mais prejudicial do que inconsistente. Deveria "apenas funcionar" como as pessoas esperam, e claramente as pessoas esperam algo diferente ao usar o foreach em vez de um loop for, dado o número de pessoas que enfrentaram problemas antes de sabermos sobre o acesso ao problema de fechamento modificado (como eu) .
Andy Andy
2
@ Random832 não sabe sobre C #, mas no Common LISP existe uma sintaxe para isso, e conclui que qualquer linguagem com variáveis ​​e fechamentos mutáveis ​​(também, deve ) também a possui. Fechamos a referência ao local em mudança ou a um valor que ele tem em um determinado momento (criação de um fechamento). Isso discute coisas semelhantes em Python e Scheme ( cutpara refs / vars e cutepara manter valores avaliados em fechamentos parcialmente avaliados).
Will Ness
103

Tendo sido mordido por isso, tenho o hábito de incluir variáveis ​​definidas localmente no escopo mais interno que uso para transferir para qualquer fechamento. No seu exemplo:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

Eu faço:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

Depois de ter esse hábito, você pode evitá-lo no caso muito raro de realmente pretender vincular-se aos escopos externos. Para ser sincero, acho que nunca fiz isso.

Godeke
fonte
24
Essa é a solução típica. Obrigado pela contribuição. O recarregador é inteligente o suficiente para reconhecer esse padrão e chamar sua atenção também, o que é bom. Eu não sou um pouco desse padrão há algum tempo, mas, como é, nas palavras de Eric Lippert, "o relatório de erro incorreto mais comum que recebemos", fiquei curioso para saber por que mais do que como evitá-lo .
precisa saber é o seguinte
62

No C # 5.0, esse problema foi corrigido e você pode fechar as variáveis ​​de loop e obter os resultados esperados.

A especificação da linguagem diz:

8.8.4 A declaração foreach

(...)

Uma declaração foreach do formulário

foreach (V v in x) embedded-statement

é então expandido para:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

(...)

A colocação de vdentro do loop while é importante para como ele é capturado por qualquer função anônima que ocorre na instrução incorporada. Por exemplo:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Se vfosse declarado fora do loop while, ele seria compartilhado entre todas as iterações, e seu valor após o loop for seria o valor final 13, que é o que a invocação de fseria impressa. Em vez disso, como cada iteração possui sua própria variável v, a capturada pela fprimeira iteração continuará mantendo o valor 7, que é o que será impresso. ( Nota: versões anteriores do C # declaradas vfora do loop while. )

Paolo Moretti
fonte
1
Por que essa versão anterior do C # declarou v dentro do loop while? msdn.microsoft.com/pt-BR/library/aa664754.aspx
colinfang
4
@colinfang Certifique-se de ler a resposta de Eric : A especificação C # 1.0 ( no seu link, estamos falando sobre o VS 2003, ou seja, C # 1.2 ) na verdade não disse se a variável de loop estava dentro ou fora do corpo do loop, pois não faz diferença observável . Quando a semântica de fechamento foi introduzida no C # 2.0, optou-se por colocar a variável do loop fora do loop, consistente com o loop "for".
Paolo Moretti
1
Então você está dizendo que os exemplos no link não eram especificações definitivas naquele momento?
colinfang
4
@colinfang Eles eram especificações definitivas. O problema é que estamos falando de um recurso (ou seja, fechamento de funções) que foi introduzido mais tarde (com C # 2.0). Quando o C # 2.0 surgiu, eles decidiram colocar a variável do loop fora do loop. E que eles mudaram de idéia novamente com C # 5.0 :)
Paolo Moretti