Acesso ao fechamento modificado (2)

101

Esta é uma extensão da pergunta de Acesso ao Fechamento Modificado . Só quero verificar se o seguinte é realmente seguro o suficiente para uso em produção.

List<string> lists = new List<string>();
//Code to retrieve lists from DB    
foreach (string list in lists)
{
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(list); });
}

Eu só repasso os itens acima uma vez por inicialização. Por enquanto, parece funcionar bem. Como Jon mencionou sobre o resultado contra-intuitivo em alguns casos. Então, o que eu preciso cuidar aqui? Tudo bem se a lista for analisada mais de uma vez?

defeituoso
fonte
18
Parabéns, agora você faz parte da documentação do Resharper. confluence.jetbrains.net/display/ReSharper/…
Kongress
1
Este foi complicado, mas a explicação acima deixou claro para mim: Isso pode parecer correto, mas, na verdade, apenas o último valor da variável str será usado sempre que qualquer botão for clicado. A razão para isso é que foreach se desdobra em um loop while, mas a variável de iteração é definida fora desse loop. Isso significa que no momento em que você mostra a caixa de mensagem, o valor de str pode já ter sido iterado para o último valor na coleção de strings.
DanielV

Respostas:

159

Antes do C # 5, você precisa declarar novamente uma variável dentro do foreach - caso contrário, ela será compartilhada e todos os seus manipuladores usarão a última string:

foreach (string list in lists)
{
    string tmp = list;
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(tmp); });
}

De forma significativa, observe que, a partir do C # 5, isso mudou e, especificamente no caso deforeach , você não precisa mais fazer isso: o código na questão funcionaria conforme o esperado.

Para mostrar que isso não está funcionando sem essa mudança, considere o seguinte:

string[] names = { "Fred", "Barney", "Betty", "Wilma" };
using (Form form = new Form())
{
    foreach (string name in names)
    {
        Button btn = new Button();
        btn.Text = name;
        btn.Click += delegate
        {
            MessageBox.Show(form, name);
        };
        btn.Dock = DockStyle.Top;
        form.Controls.Add(btn);
    }
    Application.Run(form);
}

Execute o procedimento acima antes de C # 5 e, embora cada botão mostre um nome diferente, clicar nos botões mostra "Wilma" quatro vezes.

Isso ocorre porque a especificação de idioma (ECMA 334 v4, 15.8.4) (antes de C # 5) define:

foreach (V v in x) embedded-statement é então expandido para:

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

Observe que a variável v(que é sua list) é declarada fora do loop. Portanto, pelas regras das variáveis ​​capturadas, todas as iterações da lista compartilharão o portador da variável capturada.

Do C # 5 em diante, isso mudou: a variável de iteração ( v) tem o escopo dentro do loop. Não tenho uma referência de especificação, mas basicamente se torna:

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

Cancelar a inscrição novamente; se você deseja cancelar a inscrição de um manipulador anônimo, o truque é capturar o próprio manipulador:

EventHandler foo = delegate {...code...};
obj.SomeEvent += foo;
...
obj.SomeEvent -= foo;

Da mesma forma, se você quiser um manipulador de eventos apenas uma vez (como Carregar etc):

EventHandler bar = null; // necessary for "definite assignment"
bar = delegate {
  // ... code
  obj.SomeEvent -= bar;
};
obj.SomeEvent += bar;

Isso agora está cancelando automaticamente ;-p

Marc Gravell
fonte
Se for esse o caso, a variável temporária ficará na memória até que o aplicativo feche, para servir ao delegado, e não será aconselhável fazer isso em loops muito grandes se a variável ocupar muita memória. Estou certo?
defeituoso em
1
Ele ficará na memória pelo tempo que houver coisas (botões) com o evento. Existe uma maneira de cancelar a inscrição de delegados exclusivos, que adicionarei à postagem.
Marc Gravell
2
Mas para se qualificar em seu ponto: sim, variáveis ​​capturadas podem de fato aumentar o escopo de uma variável. Você precisa ter cuidado para não capturar coisas que você não esperava ...
Marc Gravell
1
Você poderia atualizar sua resposta com relação às alterações nas especificações do C # 5.0? Apenas para torná-lo uma ótima documentação wiki sobre loops foreach em C #. Já existem algumas boas respostas sobre a mudança no compilador C # 5.0 tratando os loops foreach bit.ly/WzBV3L , mas eles não são recursos do tipo wiki.
Ilya Ivanov
1
@Kos sim, fornão mudou em 5.0
Marc Gravell