2010-02-09 10 views
20

Vengo de un fondo de programación funcional en este momento, así que perdónenme si no entiendo los cierres en C#.¿Cierres en los delegados del controlador de eventos C#?

Tengo el siguiente código para generar dinámicamente botones que obtienen los controladores de eventos anónimos:

for (int i = 0; i < 7; i++) 
{ 
    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + i); 
    }; 

    this.Controls.Add(newButton); 
} 

que esperaba el texto "I am button number " + i que ser cerrado con el valor de i en esa iteración del bucle. Sin embargo, cuando realmente ejecuto el programa, cada botón dice I am button number 7. ¿Qué me estoy perdiendo? Estoy usando VS2005.

Editar: Así que supongo que mi próxima pregunta es, ¿cómo puedo capturar el valor?

+4

No captura el valor. Nunca se capturan valores, solo variables. Para obtener más información sobre este tema, consulte http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx y http: //blogs.msdn. .com/ericlippert/archive/2009/11/16/closing-over-the-loop-variable-part-two.aspx –

Respuesta

26

Para obtener este comportamiento, tiene que copiar la variable localmente, no utilizar el iterador:

for (int i = 0; i < 7; i++) 
{ 
    var inneri = i; 
    Button newButton = new Button(); 
    newButton.Text = "Click me!"; 
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + inneri); 
    }; 
    this.Controls.Add(newButton); 
} 

El razonamiento se discute en mayor detalle in this question.

4

El cierre captura la variable, no el valor. Esto significa que para cuando se ejecuta el delegado, es decir, en algún momento después del final del ciclo, el valor de i es 6.

Para capturar un valor, asígnelo a una variable declarada en el cuerpo del bucle. En cada iteración del ciclo, se creará una nueva instancia para cada variable declarada dentro de él.

Jon Skeet's articles on closures tiene una explicación más profunda y más ejemplos.

for (int i = 0; i < 7; i++) 
{ 
    var copy = i; 

    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + copy); 
    }; 

    this.Controls.Add(newButton); 
} 
+0

-1: una respuesta más informativa que explica, en profundidad, lo que está sucediendo con un ejemplo ser calificado más alto. – IAbstract

1

En el momento de hacer clic en cualquier botón, todos ellos fueron generados a partir de 1 thru 7, por lo que todos manifiestan el estado final de I que es 7.

4

Ha creado siete delegados, pero cada delegado sostiene una referencia a la misma instancia de i.

La función MessageBox.Show solo se llama cuando se hace clic en el botón. Para cuando el botón ha presionado, el ciclo se ha completado. Entonces, en este punto i será igual a siete.

Prueba esto:

for (int i = 0; i < 7; i++) 
{ 

    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    int iCopy = i; // There will be a new instance of this created each iteration 
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + iCopy); 
    }; 

    this.Controls.Add(newButton); 
} 
23

Nick está en lo cierto, pero quería explicar un poco mejor en el texto de esta pregunta exactamente qué .

El problema no es el cierre; es el bucle for. El ciclo solo crea una variable "i" para todo el ciclo. No crea una nueva variable "i" para cada iteración. Nota: Esto ha cambiado según los informes para C# 5.

Esto significa que cuando el delegado anónimo captura o se cierra sobre ese variable "i" Es el cierre de más de una variable que es compartido por todos los botones. En el momento en que realmente haces clic en cualquiera de esos botones, el ciclo ya ha terminado de aumentar esa variable hasta 7.

La única cosa que podría hacer de manera diferente a partir del código de Nick es utilizar una cadena para la variable interna y construir todas esas cadenas en la delantera en lugar de en el tiempo de pulsación de botón, así:

for (int i = 0; i < 7; i++) 
{ 
    var message = string.Format("I am button number {0}.", i); 

    Button newButton = new Button(); 
    newButton.Text = "Click me!"; 
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show(message); 
    }; 
    this.Controls.Add(newButton); 
} 

Eso sólo comercia un poco de memoria (aferrándose a variables de cadena más grandes en lugar de enteros) por un poco de tiempo de CPU más adelante ... depende de su aplicación lo que más importa.

Otra opción es la de no codificar manualmente el bucle en absoluto:

this.Controls.AddRange(Enumerable.Range(0,7).Select(i => 
{ 
    var b = new Button() {Text = "Click me!", Top = i * 20}; 
    b.Click += (s,e) => MessageBox.Show(string.Format("I am button number {0}.", i)); 
    return b; 
}).ToArray()); 

me gusta esta última opción no tanto, ya que elimina el bucle, sino porque empieza a pensar en términos de la construcción de esta controla desde una fuente de datos.

+0

+1 para una mejora adicional! –

+0

Esto no es un error, pero lo están cambiando. Al igual que Silverlight es un marco viable (pero es posible que nunca tenga nuevas características y soporte reducido/retirado). – micahhoover

Cuestiones relacionadas