Accesso alla variabile foreach in avviso di chiusura


85

Ricevo il seguente avviso:

Accesso a foreach variabile in chiusura. Può avere un comportamento diverso se compilato con versioni diverse del compilatore.

Ecco come appare nel mio editor:

messaggio di errore sopra menzionato in un popup al passaggio del mouse

So come risolvere questo avviso, ma voglio sapere perché dovrei ricevere questo avviso?

Si tratta della versione "CLR"? È correlato a "IL"?



1
TL; Risposta DR: aggiungi .ToList () o .ToArray () alla fine dell'espressione della query e
eliminerà

Risposte:


136

Ci sono due parti in questo avviso. Il primo è ...

Accesso a foreach variabile in chiusura

... che non è valido di per sé ma è contro-intuitivo a prima vista. È anche molto difficile fare bene. (Tanto che l'articolo a cui collego di seguito lo descrive come "dannoso".)

Prendi la tua query, notando che il codice che hai estratto è fondamentalmente una forma espansa di ciò che il compilatore C # (prima di C # 5) genera per foreach1 :

[Non] capisco perché [quanto segue] non è valido:

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

Bene, è valido sintatticamente. E se tutto ciò che stai facendo nel tuo ciclo è usare il valore di, sallora tutto va bene. Ma chiudersi sporterà a un comportamento controintuitivo. Dai un'occhiata al seguente codice:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

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

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

Se esegui questo codice, otterrai il seguente output della console:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

Questo è quello che ti aspetti.

Per vedere qualcosa che probabilmente non ti aspetti, esegui il codice seguente immediatamente dopo il codice sopra:

foreach (var action in countingActions)
    action();

Otterrai il seguente output della console:

s == 5
s == 5
s == 5
s == 5
s == 5

Perché? Perché abbiamo creato cinque funzioni che fanno tutte la stessa identica cosa: stampa il valore di s(che abbiamo chiuso). In realtà, hanno la stessa funzione ("Stampa s", "Stampa s", "Stampa s" ...).

Nel momento in cui andiamo a usarli, fanno esattamente quello che chiediamo: stampare il valore di s. Se guardi l'ultimo valore noto di s, vedrai che è 5. Quindi veniamo s == 5stampati cinque volte sulla console.

Che è esattamente quello che abbiamo chiesto, ma probabilmente non quello che vogliamo.

La seconda parte dell'avvertimento ...

Può avere un comportamento diverso se compilato con versioni diverse del compilatore.

... è quello che è. A partire da C # 5, il compilatore genera codice diverso che "impedisce" che ciò accada tramiteforeach .

Pertanto il codice seguente produrrà risultati diversi in diverse versioni del compilatore:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

Di conseguenza, produrrà anche l'avviso R # :)

Il mio primo frammento di codice, sopra, mostrerà lo stesso comportamento in tutte le versioni del compilatore, poiché non lo sto usando foreach(piuttosto, l'ho espanso come fanno i compilatori pre-C # 5).

È per la versione CLR?

Non sono abbastanza sicuro di quello che stai chiedendo qui.

Il post di Eric Lippert dice che la modifica avviene "in C # 5". Cosìpresumibilmente devi scegliere come destinazione .NET 4.5 o versioni successive con un compilatore C # 5 o successivo per ottenere il nuovo comportamento e tutto ciò che precede ottiene il vecchio comportamento.

Ma per essere chiari, è una funzione del compilatore e non della versione .NET Framework.

C'è rilevanza con IL?

Un codice diverso produce un IL diverso, quindi in questo senso ci sono conseguenze per l'IL generato.

1 foreach è un costrutto molto più comune del codice che hai pubblicato nel tuo commento. Il problema si verifica in genere attraverso l'uso di foreach, non tramite l'enumerazione manuale. Ecco perché le modifiche a foreachin C # 5 aiutano a prevenire questo problema, ma non completamente.


7
In realtà ho provato il ciclo foreach su compilatori diversi ottenendo risultati diversi utilizzando lo stesso target (.Net 3.5). Ho usato VS2010 (che a sua volta utilizza il compilatore associato a .net 4.0 credo) e VS2012 (compilatore .net 4.5 credo). In linea di principio, ciò significa che se si utilizza VS2013 e si modifica un progetto con destinazione .Net 3.5 e lo si crea su un server di compilazione su cui è installato un framework leggermente più vecchio, è possibile vedere risultati diversi del programma sulla macchina rispetto alla build distribuita.
Ykok

Buona risposta, ma non sono sicuro di quanto "foreach" sia rilevante. Non succederebbe con l'enumerazione manuale o anche con un semplice ciclo for (int i = 0; i <collection.Size; i ++)? Sembra essere un problema con le chiusure che escono dall'ambito, o più precisamente, un problema con le persone che capiscono come si comportano le chiusure quando escono dall'ambito in cui sono state definite all'interno.
Brad

Le foreachcose qui provengono dal contenuto della domanda. Hai ragione che può accadere in vari modi, più generali.
ta.speot.is

1
Perché R # mi avvisa ancora, non legge il framework di destinazione, che ho impostato su 4.5.
Johnny_D

1
"Quindi presumibilmente devi scegliere come destinazione .NET 4.5 o versioni successive" Questa affermazione non è vera. La versione di .NET di destinazione non ha alcun effetto su questo, il comportamento viene modificato anche in .NET 2.0, 3.5 e 4 se si utilizza C # 5 (VS 2012 o più recente) per la compilazione. Questo è il motivo per cui ricevi questo avviso solo su .NET 4.0 o versioni precedenti, se scegli come destinazione 4.5 non ricevi l'avviso perché non puoi compilare 4.5 su un compilatore C # 4 o precedente.
Scott Chamberlain,

12

La prima risposta è ottima, quindi ho pensato di aggiungere solo una cosa.

Stai ricevendo l'avviso perché, nel tuo codice di esempio, a ReflectionModel viene assegnato un IEnumerable, che verrà valutato solo al momento dell'enumerazione e l'enumerazione stessa potrebbe avvenire al di fuori del ciclo se hai assegnato ReflectionModel a qualcosa con un ambito più ampio .

Se sei cambiato

...Where(x => x.Name == property.Value)

per

...Where(x => x.Name == property.Value).ToList()

quindi a reflectionModel verrà assegnato un elenco definito all'interno del ciclo foreach, quindi non riceverai l'avviso, poiché l'enumerazione avverrebbe sicuramente all'interno del ciclo e non al di fuori di esso.


Ho letto molte spiegazioni molto lunghe che non mi hanno risolto questo problema, poi una breve che lo ha fatto. Grazie!
Charles Clayton

Ho letto la risposta accettata e ho pensato "com'è una chiusura se non vincola le variabili?" ma ora capisco che si tratta di quando avviene la valutazione, grazie!
Jerome

Sì, questa è un'ovvia soluzione universale. Lento, ad alta intensità di memoria, ma penso che funzioni davvero al 100% per tutti i casi.
Al Kepp,

8

Una variabile con ambito di blocco dovrebbe risolvere l'avviso.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
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.