C’è un motivo per il riuso della variabile C # in un foreach?

Quando si usano espressioni lambda o metodi anonimi in C #, dobbiamo essere cauti sull’accesso ai trabocchetti di chiusura modificati . 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 farà sì che tutte le clausole Where sulla query siano basate sul valore finale di s .

Come spiegato qui , ciò accade perché la variabile s dichiarata nel ciclo foreach 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 indicato qui , non ci sono vantaggi in termini di prestazioni nel dichiarare una variabile al di fuori del ciclo, e in circostanze normali l’unica ragione per cui posso pensare è se si prevede di utilizzare 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 ciclo foreach non possono essere utilizzate al di fuori del ciclo:

 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 lo rende molto incline a un errore che è spesso difficile da trovare ed eseguire il debug, senza produrre alcun beneficio percepibile.

C’è qualcosa che puoi fare con i cicli foreach questo modo che non puoi se sono stati compilati con una variabile con ambito interno, o questa è solo una scelta arbitraria che è stata fatta prima che i metodi anonimi e le espressioni lambda fossero disponibili o comuni, e quali non è stato rivisto da allora?

Il compilatore dichiara la variabile in un modo che lo rende molto incline a un errore che è spesso difficile da trovare e correggere, mentre non produce alcun beneficio percepibile.

Le tue critiche sono completamente giustificate.

Discuto questo problema in dettaglio qui:

Chiusura sulla variabile del ciclo considerata dannosa

C’è qualcosa che puoi fare con foreach loop in questo modo che non potresti fare se sono stati 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 siano stati rivisti da allora?

L’ultimo. La specifica C # 1.0 in realtà non diceva se la variabile loop era 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, la scelta è stata fatta per mettere la variabile di loop al di fuori del ciclo, in modo coerente con il ciclo “for”.

Penso che sia giusto dire che tutti rimpiangono questa decisione. Questo è uno dei peggiori “trucchi” in C #, e stiamo andando a prendere il cambiamento decisivo per risolverlo. In C # 5 la variabile del ciclo foreach sarà logicamente all’interno del corpo del ciclo e pertanto le chiusure riceveranno una nuova copia ogni volta.

Il ciclo for non verrà modificato e la modifica non sarà “back ported” alle versioni precedenti di C #. Dovresti quindi continuare a stare attento quando usi questo idioma.

Quello che stai chiedendo è completamente coperto da Eric Lippert nel suo post sul blog Closing over the loop variable considerato nocivo e il suo seguito.

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

Il problema più comune con questo comportamento è fare una chiusura sulla variabile di iterazione e ha una soluzione facile:

 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 della variabile foreach in C # .

Essendo stato morso da questo, ho l’abitudine di includere variabili definite localmente nello scope più interno che uso 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 questa abitudine, puoi evitarlo nel caso molto raro che intendessi effettivamente associare agli ambiti esterni. Ad essere onesti, non penso di averlo mai fatto.

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

Le specifiche della lingua dicono:

8.8.4 La dichiarazione di foreach

(…)

Una dichiarazione foreach del modulo

 foreach (V v in x) embedded-statement 

viene quindi esteso a:

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

(…)

Il posizionamento di v all’interno del ciclo while è importante per il modo in cui viene catturato da qualsiasi funzione anonima presente nell’istruzione embedded. 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 v stato 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 verrà stampata l’invocazione di f . Invece, poiché ogni iterazione ha una propria variabile v , quella catturata da f nella prima iterazione continuerà a contenere il valore 7 , che è ciò che verrà stampato. ( Nota: versioni precedenti di C # hanno dichiarato v al di fuori del ciclo while. )

Secondo me è una domanda strana. È bello sapere come funziona il compilatore ma solo “buono a sapersi”.

Se scrivi codice che dipende dall’algoritmo del compilatore è una ctriggers pratica. Ed è meglio riscrivere il codice per escludere questa dipendenza.

Questa è una buona domanda per il colloquio di lavoro. Ma nella vita reale non ho avuto problemi con il mio colloquio di lavoro.

Il 90% di foreach utilizza per elaborare ogni elemento della raccolta (non per selezionare o calcolare alcuni valori). a volte è necessario calcolare alcuni valori all’interno del ciclo, ma non è consigliabile creare un ciclo BIG.

È meglio usare le espressioni LINQ per calcolare i valori. Perché quando calcoli molte cose all’interno del ciclo, dopo 2-3 mesi quando tu (o chiunque altro) leggerà questo codice, la persona non capirà cosa è e come dovrebbe funzionare.