C'è una ragione per il riutilizzo di C # della variabile in una foreach?


1685

Quando si usano espressioni lambda o metodi anonimi in C #, dobbiamo stare attenti all'accesso alla trappola della chiusura modificata . Per esempio:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

A causa della chiusura modificata, il codice sopra riportato farà sì che tutte le Whereclausole sulla query siano basate sul valore finale di s.

Come spiegato qui , questo accade perché la svariabile dichiarata nel foreachciclo sopra è tradotta in questo modo nel compilatore:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

invece di così:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

Come sottolineato qui , non ci sono vantaggi prestazionali nel dichiarare una variabile al di fuori del ciclo, e in circostanze normali l'unica ragione a cui posso pensare per farlo è se pensi di usare la variabile al di fuori dell'ambito del ciclo:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Tuttavia, le variabili definite in un foreachloop non possono essere utilizzate al di fuori del loop:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Quindi il compilatore dichiara la variabile in un modo che la rende altamente soggetta a un errore che è spesso difficile da trovare e eseguire il debug, senza produrre benefici percepibili.

C'è qualcosa che puoi fare con i foreachloop in questo modo che non potresti se fossero compilati con una variabile con ambito interno, o è solo una scelta arbitraria che è stata fatta prima che i metodi anonimi e le espressioni lambda fossero disponibili o comuni, e che non ha da allora è stato rivisto?


4
Cosa c'è che non va String s; foreach (s in strings) { ... }?
Brad Christie,

5
@BradChristie l'OP non sta davvero parlando, foreachma di espressioni lamda che danno come risultato un codice simile a quello mostrato dall'OP ...
Yahia,

22
@BradChristie: si compila? ( Errore: il tipo e l'identificatore sono entrambi richiesti in una dichiarazione foreach per me)
Austin Salonen,

32
@JakobBotschNielsen: è un locale esterno chiuso di una lambda; perché stai supponendo che sarà in pila? La sua durata è più lunga del frame dello stack !
Eric Lippert,

3
@EricLippert: sono confuso. Capisco che lambda acquisisce un riferimento alla variabile foreach (che è dichiarata internamente al di fuori del ciclo) e quindi si finisce per confrontarla con il suo valore finale; che ottengo. Quello che non capisco è come dichiarare la variabile all'interno del ciclo farà alcuna differenza. Dal punto di vista del compilatore-scrittore, sto assegnando solo un riferimento di stringa (var 's') nello stack, indipendentemente dal fatto che la dichiarazione sia all'interno o all'esterno del ciclo; Certamente non vorrei inserire un nuovo riferimento nello stack ad ogni iterazione!
Anthony,

Risposte:


1407

Il compilatore dichiara la variabile in un modo che la rende altamente soggetta a un errore che è spesso difficile da trovare e eseguire il debug, senza produrre benefici percepibili.

La tua critica è del tutto giustificata.

Discuto questo problema in dettaglio qui:

Chiudere la variabile loop considerata dannosa

C'è qualcosa che puoi fare con foreach loop in questo modo che non potresti se fossero compilati con una variabile con ambito interno? o è solo una scelta arbitraria fatta prima che fossero disponibili o comuni metodi anonimi ed espressioni lambda e che da allora non è stata rivista?

L'ultimo. La specifica C # 1.0 in realtà non diceva se la variabile del loop fosse all'interno o all'esterno del corpo del loop, poiché non faceva alcuna differenza osservabile. Quando la semantica di chiusura è stata introdotta in C # 2.0, è stata fatta la scelta di mettere la variabile del ciclo al di fuori del ciclo, coerentemente con il ciclo "for".

Penso che sia giusto dire che tutti rimpiangono questa decisione. Questo è uno dei peggiori "gotchas" in C #, e prenderemo il cambiamento per risolverlo. In C # 5 la variabile del ciclo foreach sarà logicamente all'interno del corpo del ciclo, e quindi le chiusure riceveranno una nuova copia ogni volta.

Il forciclo non verrà modificato e la modifica non verrà "trasferita" alle versioni precedenti di C #. Dovresti quindi continuare a stare attento quando usi questo linguaggio.


177
In effetti abbiamo respinto questo cambiamento in C # 3 e C # 4. Quando abbiamo progettato C # 3 ci siamo resi conto che il problema (che già esisteva in C # 2) stava peggiorando perché ci sarebbero stati così tanti lambda (e query) comprensioni, che sono lambda sotto mentite spoglie) nei cicli foreach grazie a LINQ. Mi dispiace che abbiamo aspettato che il problema diventasse sufficientemente grave da giustificare la sua correzione così tardi, piuttosto che risolverlo in C # 3.
Eric Lippert,

75
E ora dovremo ricordare che foreachè "sicuro" ma fornon lo è.
Leppie,

22
@michielvoo: il cambiamento si sta rompendo nel senso che non è compatibile con le versioni precedenti. Il nuovo codice non verrà eseguito correttamente quando si utilizza un compilatore più vecchio.
leppie,

41
@Benjol: No, ecco perché siamo disposti a prenderlo. Jon Skeet mi ha fatto notare un importante scenario di cambiamento, ovvero che qualcuno scrive codice in C # 5, lo testa e poi lo condivide con persone che stanno ancora usando C # 4, che poi ingenuamente credono che sia corretto. Speriamo che il numero di persone colpite da tale scenario sia piccolo.
Eric Lippert,

29
A parte questo, ReSharper l'ha sempre colto e lo segnala come "accesso alla chiusura modificata". Quindi, premendo Alt + Invio, risolverà anche automaticamente il codice per te. jetbrains.com/resharper
Mike Chamberlain,

191

Quello che stai chiedendo è completamente coperto da Eric Lippert nel suo post sul blog. Chiudere la variabile loop considerata dannosa e il suo sequel.

Per me, l'argomento più convincente è che avere una nuova variabile in ogni iterazione sarebbe incompatibile con il for(;;)loop di stile. Ti aspetteresti di avere un nuovo int iin ogni iterazione di for (int i = 0; i < 10; i++)?

Il problema più comune con questo comportamento è la chiusura di una variabile di iterazione e una soluzione semplice:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Il mio post sul blog su questo problema: chiusura per foreach variabile in C # .


18
In definitiva, ciò che le persone vogliono effettivamente quando scrivono questo non deve avere più variabili, è chiudere il valore . Ed è difficile pensare a una sintassi utilizzabile per questo nel caso generale.
Casuale 832,

1
Sì, non è possibile chiudere il valore, tuttavia esiste una soluzione molto semplice che ho appena modificato la mia risposta per includerla.
Krizz,

6
Peccato che chiusure in C # si chiudano sui riferimenti. Se per impostazione predefinita hanno chiuso i valori, potremmo facilmente specificare la chiusura delle variabili anziché ref.
Sean U

2
@Krizz, questo è un caso in cui la coerenza forzata è più dannosa che incoerente. Dovrebbe "semplicemente funzionare" come le persone si aspettano, e chiaramente le persone si aspettano qualcosa di diverso quando usano foreach invece di un ciclo for, dato il numero di persone che hanno avuto problemi prima di sapere dell'accesso al problema di chiusura modificato (come me stesso) .
Andy,

2
@ Random832 non sa di C # ma in Common LISP esiste una sintassi per questo, e sembra che qualsiasi linguaggio con variabili e chiusure mutabili dovrebbe (anzi, deve ) averlo. O chiudiamo il riferimento al luogo che cambia, o un valore che ha in un dato momento (creazione di una chiusura). Questo illustra roba simile in Python e Scheme ( cutper refs / VAR e cuteper mantenere valutati valori chiusure parzialmente valutati).
Will Ness,

103

Essendo stato morso da questo, ho l'abitudine di includere variabili localmente definite nell'ambito più interno che utilizzo per trasferire a qualsiasi chiusura. Nel tuo esempio:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

Lo voglio:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

Una volta che hai quell'abitudine, puoi evitarlo nel caso molto raro in cui intendevi legarti agli ambiti esterni. Ad essere sincero, non credo di averlo mai fatto.


24
Questa è la soluzione alternativa tipica Grazie per il contributo. Resharper è abbastanza intelligente da riconoscere questo modello e portarlo anche alla tua attenzione, il che è bello. Non sono stato morso da questo schema da un po 'di tempo, ma poiché è, nelle parole di Eric Lippert, "il singolo bug report errato più comune che riceviamo", ero curioso di sapere il perché più che il modo di evitarlo .
StriplingWarrior il

62

In C # 5.0, questo problema è stato risolto e puoi chiudere le variabili di loop e ottenere i risultati che ti aspetti.

La specifica della lingua dice:

8.8.4 L'istruzione foreach

(...)

Una dichiarazione foreach del modulo

foreach (V v in x) embedded-statement

viene quindi espanso in:

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

(...)

Il posizionamento vall'interno del ciclo while è importante per come viene catturato da qualsiasi funzione anonima presente nell'istruzione incorporata. Per esempio:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Se vfosse dichiarato al di fuori del ciclo while, sarebbe condiviso tra tutte le iterazioni e il suo valore dopo il ciclo for sarebbe il valore finale 13, che è ciò che l'invocazione fdovrebbe stampare. Invece, poiché ogni iterazione ha una propria variabile v, quella catturata dalla fprima iterazione continuerà a contenere il valore 7, che è ciò che verrà stampato. ( Nota: le versioni precedenti di C # dichiarate val di fuori del ciclo while. )


1
Perché questa prima versione di C # ha dichiarato v all'interno del ciclo while? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang

4
@colinfang Assicurati di leggere la risposta di Eric : La specifica C # 1.0 ( nel tuo link stiamo parlando di VS 2003, ovvero C # 1.2 ) in realtà non ha detto se la variabile loop era all'interno o all'esterno del corpo del loop, in quanto non fa alcuna differenza osservabile . Quando la semantica di chiusura è stata introdotta in C # 2.0, è stata fatta la scelta di mettere la variabile del ciclo al di fuori del ciclo, coerentemente con il ciclo "for".
Paolo Moretti,

1
Quindi stai dicendo che gli esempi nel link non erano specifiche definitive in quel momento?
Colinfang,

4
@colinfang Erano specifiche definitive. Il problema è che stiamo parlando di una funzionalità (ovvero la chiusura di funzioni) che è stata introdotta in seguito (con C # 2.0). Quando è arrivato C # 2.0, hanno deciso di mettere la variabile loop fuori dal loop. E poi hanno cambiato di nuovo idea con C # 5.0 :)
Paolo Moretti,
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.