Acceso al cierre modificado (2)

101

Esta es una extensión de la pregunta de Acceso a Cierre Modificado . Solo quiero verificar si lo siguiente es realmente lo suficientemente seguro para su uso en producción.

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

Solo ejecuto lo anterior una vez por inicio. Por ahora parece funcionar bien. Como Jon ha mencionado sobre el resultado contradictorio en algún caso. Entonces, ¿qué debo tener cuidado aquí? ¿Estará bien si la lista se revisa más de una vez?

defectuoso
fuente
18
Felicitaciones, ahora eres parte de la documentación de Resharper. confluence.jetbrains.net/display/ReSharper/…
Kongress
1
Este fue complicado, pero la explicación anterior me lo dejó claro: esto puede parecer correcto pero, en realidad, solo se usará el último valor de la variable str cada vez que se haga clic en cualquier botón. La razón de esto es que foreach se desenrolla en un ciclo while, pero la variable de iteración se define fuera de este ciclo. Esto significa que para cuando muestre el cuadro de mensaje, es posible que el valor de str ya se haya iterado hasta el último valor de la colección de cadenas.
DanielV

Respuestas:

159

Antes de C # 5, debe volver a declarar una variable dentro de foreach; de lo contrario, se comparte y todos sus controladores usarán la última cadena:

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

Significativamente, tenga en cuenta que desde C # 5 en adelante, esto ha cambiado, y específicamente en el caso deforeach , ya no necesita hacer esto: el código en la pregunta funcionaría como se esperaba.

Para mostrar que esto no funciona sin este cambio, considere lo siguiente:

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

Ejecute lo anterior antes de C # 5 , y aunque cada botón muestra un nombre diferente, al hacer clic en los botones se muestra "Wilma" cuatro veces.

Esto se debe a que la especificación de lenguaje (ECMA 334 v4, 15.8.4) (antes de C # 5) define:

foreach (V v in x) embedded-statement luego se expande a:

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

Tenga en cuenta que la variable v(que es su list) se declara fuera del ciclo. Entonces, según las reglas de las variables capturadas, todas las iteraciones de la lista compartirán el titular de la variable capturada.

Desde C # 5 en adelante, esto se cambia: la variable de iteración ( v) tiene un alcance dentro del ciclo. No tengo una referencia de especificación, pero básicamente se convierte en:

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

Volver a cancelar la suscripción; Si desea cancelar activamente la suscripción de un controlador anónimo, el truco consiste en capturar el controlador en sí:

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

Del mismo modo, si desea un controlador de eventos único (como Load, etc.):

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

Esto ahora es auto-cancelación de suscripción ;-p

Marc Gravell
fuente
Si ese es el caso, la variable temporal permanecerá en la memoria hasta que se cierre la aplicación, para servir al delegado, y no será recomendable hacerlo para bucles muy grandes si la variable ocupa mucha memoria. Estoy en lo cierto?
defectuoso
1
Permanecerá en la memoria durante el tiempo que haya elementos (botones) con el evento. Hay una forma de cancelar la suscripción de los delegados que solo una vez, que agregaré a la publicación.
Marc Gravell
2
Pero para calificar en su punto: sí, las variables capturadas pueden aumentar el alcance de una variable. Debes tener cuidado de no capturar cosas que no esperabas ...
Marc Gravell
1
¿Podría actualizar su respuesta con respecto al cambio en la especificación C # 5.0? Solo para que sea una excelente documentación wiki sobre los bucles foreach en C #. Ya hay algunas buenas respuestas con respecto al cambio en el compilador de C # 5.0 que trata los bucles foreach bit.ly/WzBV3L , pero no son recursos de tipo wiki.
Ilya Ivanov
1
@Kos sí, no forha cambiado en 5.0
Marc Gravell