Accesso alla chiusura modificata (2)


101

Questa è un'estensione della domanda dall'accesso alla chiusura modificata . Voglio solo verificare se quanto segue è effettivamente abbastanza sicuro per l'uso in produzione.

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

Eseguo quanto sopra solo una volta per avvio. Per ora sembra funzionare bene. Come ha detto Jon, in alcuni casi il risultato controintuitivo. Allora a cosa devo fare attenzione qui? Va bene se l'elenco viene eseguito più di una volta?


18
Congratulazioni, ora fai parte della documentazione di Resharper. confluence.jetbrains.net/display/ReSharper/…
Kongress

1
Questo è stato complicato, ma la spiegazione sopra mi ha reso chiaro: questo può sembrare corretto ma, in realtà, verrà utilizzato solo l'ultimo valore della variabile str ogni volta che si fa clic su un pulsante. La ragione di ciò è che foreach si srotola in un ciclo while, ma la variabile di iterazione è definita al di fuori di questo ciclo. Ciò significa che nel momento in cui viene visualizzata la finestra di messaggio, il valore di str potrebbe essere già stato iterato all'ultimo valore nella raccolta di stringhe.
DanielV

Risposte:


159

Prima di C # 5, è necessario dichiarare nuovamente una variabile all'interno di foreach, altrimenti è condivisa e tutti i gestori useranno l'ultima stringa:

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

È significativo notare che da C # 5 in poi, questo è cambiato e, in particolare, nel caso diforeach , non è più necessario farlo: il codice nella domanda funzionerebbe come previsto.

Per dimostrare che questo non funziona senza questa modifica, considera quanto segue:

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

Eseguire quanto sopra prima di C # 5 e, sebbene ogni pulsante mostri un nome diverso, facendo clic sui pulsanti viene visualizzato "Wilma" quattro volte.

Questo perché la specifica della lingua (ECMA 334 v4, 15.8.4) (prima di C # 5) definisce:

foreach (V v in x) embedded-statement viene quindi espanso a:

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

Nota che la variabile v(che è tua list) è dichiarata al di fuori del ciclo. Quindi, secondo le regole delle variabili catturate, tutte le iterazioni della lista condivideranno il detentore della variabile catturata.

Da C # 5 in poi, questo è cambiato: la variabile di iterazione ( v) ha l'ambito all'interno del ciclo. Non ho un riferimento alle specifiche, ma fondamentalmente diventa:

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

Annullare nuovamente l'iscrizione; se vuoi attivamente annullare l'iscrizione a un gestore anonimo, il trucco è acquisire il gestore stesso:

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

Allo stesso modo, se vuoi un gestore di eventi una sola volta (come Load ecc.):

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

Questo è ora l'auto-annullamento dell'iscrizione ;-p


In tal caso, la variabile temporanea rimarrà in memoria fino alla chiusura dell'app, in modo da servire il delegato, e non sarà consigliabile farlo per cicli molto grandi se la variabile occupa molta memoria. Ho ragione?
difettoso il

1
Rimarrà in memoria per il tempo in cui ci sono cose (pulsanti) con l'evento. C'è un modo per annullare la sottoscrizione di delegati una tantum, che aggiungerò al post.
Marc Gravell

2
Ma per qualificarti sul tuo punto: sì, le variabili catturate possono effettivamente aumentare l'ambito di una variabile. Devi stare attento a non catturare cose che non ti aspettavi ...
Marc Gravell

1
Potresti aggiornare la tua risposta rispetto alla modifica nelle specifiche C # 5.0? Solo per renderlo un'ottima documentazione wiki per quanto riguarda i cicli foreach in C #. Ci sono già alcune buone risposte riguardo alla modifica nel compilatore C # 5.0 che tratta i cicli foreach bit.ly/WzBV3L , ma non sono risorse simili a wiki.
Ilya Ivanov

1
@Kos sì, forè invariato in 5.0
Marc Gravell
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.