2010-02-09 9 views
20

Attualmente vengo da uno sfondo di programmazione funzionale, quindi perdonami se non capisco le chiusure in C#.Chiusure nei delegati del gestore di eventi C#?

Ho il seguente codice per generare dinamicamente i pulsanti che ottengono i gestori di eventi anonimi:

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

mi aspettavo il testo "I am button number " + i essere chiuso con il valore di i in quel iterazione del ciclo for. Tuttavia, quando eseguo effettivamente il programma, ogni pulsante dice I am button number 7. Cosa mi manca? Sto usando VS2005.

Modifica: Quindi suppongo che la mia prossima domanda sia: come posso acquisire il valore?

+4

Non si acquisisce il valore. Non acquisisci mai valori, solo variabili. Per ulteriori informazioni su questo problema, vedere http://blogs.msdn.com/ericlippert/archive/2009/11/12/close-over-the-loop-variable-considered-harmful.aspx e http: //blogs.msdn .com/ericlippert/archive/2009/11/16/closing-over-the-loop-variable-part-two.aspx –

risposta

26

Per ottenere questo comportamento, è necessario copiare la variabile localmente, non utilizzare l'iteratore:

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

Il ragionamento è discusso in modo molto più dettagliato in this question.

4

La chiusura acquisisce la variabile non il valore. Ciò significa che al momento dell'esecuzione del delegato, vale a dire dopo la fine del ciclo, il valore di i è 6.

Per acquisire un valore, assegnarlo a una variabile dichiarata nel corpo del ciclo. Ad ogni iterazione del ciclo, verrà creata una nuova istanza per ogni variabile dichiarata al suo interno.

Jon Skeet's articles on closures ha una spiegazione più approfondita e più esempi.

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 risposta più informativa che spiega, in modo approfondito, cosa sta succedendo con un esempio essere valutato più alto. – IAbstract

1

Con il tempo si fa clic su un pulsante, tutti sono stati generati 1-7, in modo che saranno tutti esprimere lo stato finale di I che è 7.

4

È stato creato sette delegati, ma ogni delegato tiene un riferimento alla stessa istanza di i.

La funzione MessageBox.Show si chiama solo quando si fa clic sul pulsante. Quando il pulsante ha fatto clic, il ciclo è completato. Quindi, a questo punto, i sarà uguale a sette.

Prova questo:

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 ha ragione, ma ho voluto spiegare un po 'meglio nel testo di questa domanda esattamente perché.

Il problema non è la chiusura; è il ciclo for. Il ciclo crea solo una variabile "i" per l'intero ciclo. Non crea una nuova variabile "i" per ogni iterazione. Nota: Questo ha riferito cambiato per C# 5.

Questo significa che quando il delegato anonima cattura o chiude su quella "i" variabile Si sta chiudendo su una variabile che è condivisa da tutti i pulsanti. Nel momento in cui effettivamente si fa clic su uno di questi pulsanti, il ciclo ha già completato l'incremento di tale variabile fino a 7.

L'unica cosa che potrei fare in modo diverso dal codice di Nick è usare una stringa per la variabile interna e costruire tutte quelle corde in anticipo, piuttosto che al momento della pressione del pulsante, in questo modo:

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

Che proprio commercia un po 'di memoria (tenendo conto di variabili stringa più grandi invece di numeri interi) per un po' di tempo in poi della cpu ... dipende dalla vostra applicazione ciò che conta di più.

Un'altra opzione è quella di non codificare manualmente il circuito a tutti:

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

Mi piace questa ultima opzione non tanto perché rimuove l'anello, ma perché inizia pensando in termini di costruzione di questa regola da un fonte di dati.

+0

+1 per ulteriori miglioramenti! –

+0

Questo non è un bug ma lo stanno cambiando. Proprio come Silverlight è un framework praticabile (ma potrebbe non avere mai nuove funzionalità e supporto ridotto/ritirato). – micahhoover

Problemi correlati