Accès à la variable foreach dans l'avertissement de fermeture

86

Je reçois l'avertissement suivant:

Accès à foreach variable en fermeture. Peut avoir un comportement différent lorsqu'il est compilé avec différentes versions du compilateur.

Voici à quoi cela ressemble dans mon éditeur:

message d'erreur mentionné ci-dessus dans une fenêtre contextuelle de survol

Je sais comment résoudre cet avertissement, mais je veux savoir pourquoi j'obtiendrais cet avertissement?

S'agit-il de la version "CLR"? Est-ce lié à "IL"?

Jeroen
la source
1
TL; DR réponse: ajoutez .ToList () ou .ToArray () à la fin de votre expression de requête et cela éliminera l'avertissement
JoelFan

Réponses:

136

Cet avertissement comporte deux parties. Le premier est ...

Accès à foreach variable en fermeture

... ce qui n'est pas invalide en soi, mais il est contre-intuitif à première vue. Il est également très difficile de bien faire. (À tel point que l'article auquel je renvoie ci-dessous décrit cela comme "nuisible".)

Prenez votre requête, en notant que le code que vous avez extrait est essentiellement une forme développée de ce que le compilateur C # (avant C # 5) génère pour foreach1 :

Je [ne] comprends pas pourquoi [ce qui suit n'est] pas valide:

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

Eh bien, il est syntaxiquement valide. Et si tout ce que vous faites dans votre boucle utilise la valeur de salors tout est bon. Mais la fermeture smènera à un comportement contre-intuitif. Jetez un œil au code suivant:

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

Si vous exécutez ce code, vous obtiendrez la sortie de console suivante:

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

C'est ce à quoi vous vous attendez.

Pour voir quelque chose auquel vous ne vous attendez probablement pas, exécutez le code suivant immédiatement après le code ci-dessus:

foreach (var action in countingActions)
    action();

Vous obtiendrez la sortie de console suivante:

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

Pourquoi? Parce que nous avons créé cinq fonctions qui font toutes exactement la même chose: afficher la valeur de s(que nous avons clôturée). En réalité, il s'agit de la même fonction ("Imprimer s", "Imprimer s", "Imprimer s" ...).

Au moment où nous allons les utiliser, ils font exactement ce que nous demandons: imprimer la valeur de s. Si vous regardez la dernière valeur connue de s, vous verrez que c'est 5. Nous sommes donc s == 5imprimés cinq fois sur la console.

C'est exactement ce que nous avons demandé, mais probablement pas ce que nous voulons.

La deuxième partie de l'avertissement ...

Peut avoir un comportement différent lorsqu'il est compilé avec différentes versions du compilateur.

... c'est ce que c'est. À partir de C # 5, le compilateur génère un code différent qui "empêche" que cela se produise viaforeach .

Ainsi, le code suivant produira des résultats différents sous différentes versions du compilateur:

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

Par conséquent, il produira également l'avertissement R # :)

Mon premier extrait de code, ci-dessus, présentera le même comportement dans toutes les versions du compilateur, puisque je n'utilise pas foreach(je l'ai plutôt développé comme le font les compilateurs pré-C # 5).

Est-ce pour la version CLR?

Je ne sais pas trop ce que vous demandez ici.

Le message d'Eric Lippert indique que le changement se produit "en C # 5". Doncvous devez probablement cibler .NET 4.5 ou version ultérieure avec un compilateur C # 5 ou version ultérieure pour obtenir le nouveau comportement, et tout ce qui précède obtient l'ancien comportement.

Mais pour être clair, c'est une fonction du compilateur et non de la version .NET Framework.

Y a-t-il une pertinence avec IL?

Un code différent produit un IL différent, donc dans ce sens il y a des conséquences pour l'IL généré.

1 foreach est une construction beaucoup plus courante que le code que vous avez publié dans votre commentaire. Le problème survient généralement par l'utilisation de foreach, et non par une énumération manuelle. C'est pourquoi les modifications apportées à foreachC # 5 permettent d'éviter ce problème, mais pas complètement.

ta.speot.is
la source
7
J'ai en fait essayé la boucle foreach sur différents compilateurs pour obtenir des résultats différents en utilisant la même cible (.Net 3.5). J'ai utilisé VS2010 (qui à son tour utilise le compilateur associé à .net 4.0 je crois) et VS2012 (compilateur .net 4.5 je crois). En principe, cela signifie que si vous utilisez VS2013 et que vous modifiez un projet ciblant .Net 3.5 et que vous le construisez sur un serveur de build sur lequel un framework légèrement plus ancien est installé, vous pouvez voir des résultats différents de votre programme sur votre machine par rapport à la build déployée.
Ykok
Bonne réponse, mais je ne sais pas comment "foreach" est pertinent. Cela ne se produirait-il pas avec une énumération manuelle, ou même une simple boucle for (int i = 0; i <collection.Size; i ++)? Cela semble être un problème avec les fermetures qui sortent du cadre, ou plus exactement, un problème avec les gens qui comprennent comment les fermetures se comportent quand elles sortent du champ dans lequel elles ont été définies.
Brad
Le foreachtruc ici vient du contenu de la question. Vous avez raison de dire que cela peut se produire de diverses manières, plus générales.
ta.speot.is le
1
Pourquoi R # me prévient toujours, ne lit-il pas le framework cible, que j'ai mis à 4.5.
Johnny_D
1
"Vous devez donc probablement cibler .NET 4.5 ou version ultérieure" Cette affirmation n'est pas vraie. La version de .NET que vous ciblez n'a aucun effet sur cela, le comportement est également modifié dans .NET 2.0, 3.5 et 4 si vous utilisez C # 5 (VS 2012 ou plus récent) pour compiler. C'est pourquoi vous n'obtenez cet avertissement que sur .NET 4.0 ou version antérieure, si vous ciblez 4.5, vous ne recevez pas l'avertissement car vous ne pouvez pas compiler 4.5 sur un compilateur C # 4 ou antérieur.
Scott Chamberlain
12

La première réponse est excellente, alors j'ai pensé ajouter juste une chose.

Vous recevez l'avertissement car, dans votre exemple de code, ReflectModel se voit attribuer un IEnumerable, qui ne sera évalué qu'au moment de l'énumération, et l'énumération elle-même peut se produire en dehors de la boucle si vous affectez ReflectModel à quelque chose avec une portée plus large .

Si vous avez changé

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

à

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

alors ReflectModel se verrait attribuer une liste définie dans la boucle foreach, vous ne recevrez donc pas l'avertissement, car l'énumération se produirait certainement dans la boucle, et non en dehors.

David
la source
J'ai lu beaucoup d'explications très longues qui n'ont pas résolu ce problème pour moi, puis une courte qui l'a fait. Merci!
Charles Clayton
J'ai lu la réponse acceptée et j'ai juste pensé "comment est-ce une fermeture si elle ne lie pas les variables?" mais maintenant je comprends que c'est quand l'évaluation a lieu, merci!
Jerome
Oui, c'est une solution universelle évidente. Lent, gourmand en mémoire, mais je pense que cela fonctionne vraiment à 100% dans tous les cas.
Al Kepp
8

Une variable à portée de bloc doit résoudre l'avertissement.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Dmitry Gogol
la source