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.