web-dev-qa-db-fra.com

Y a-t-il une raison pour que C # réutilise la variable dans un foreach?

Lorsque vous utilisez des expressions lambda ou des méthodes anonymes en C #, vous devez vous méfier des accès aux pièges de fermeture modifiés . Par exemple:

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

En raison de la fermeture modifiée, le code ci-dessus fera que toutes les clauses Where de la requête seront basées sur la valeur finale de s.

Comme expliqué ici , cela se produit car la variable s déclarée dans la boucle foreach ci-dessus est traduite de la manière suivante dans le compilateur:

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

au lieu de ceci:

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

Comme indiqué ici , il n'y a aucun avantage en termes de performances à déclarer une variable en dehors de la boucle et, dans des circonstances normales, la seule raison pour laquelle je peux penser à cela est si vous envisagez d'utiliser la variable en dehors de la portée. de la boucle:

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

Cependant, les variables définies dans une boucle foreach ne peuvent pas être utilisées en dehors de la boucle:

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

Le compilateur déclare donc la variable de manière à la rendre très sujette à une erreur souvent difficile à trouver et à déboguer, tout en ne produisant aucun avantage perceptible.

Y a-t-il quelque chose que vous pouvez faire avec les boucles foreach de cette manière que vous ne pourriez pas utiliser si elles ont été compilées avec une variable de portée interne, ou s'agit-il simplement d'un choix arbitraire effectué avant la disponibilité des méthodes anonymes et des expressions lambda ou commun, et qui n'a pas été révisé depuis?

1614
StriplingWarrior

Le compilateur déclare la variable de manière à la rendre très sujette à une erreur souvent difficile à trouver et à déboguer, tout en ne produisant aucun avantage perceptible.

Votre critique est entièrement justifiée.

Je discute de ce problème en détail ici:

La variable de fermeture sur la boucle considérée comme nuisible

Y a-t-il quelque chose que vous pouvez faire avec les boucles foreach de cette manière que vous ne pourriez pas faire si elles étaient compilées avec une variable à portée interne? ou est-ce juste un choix arbitraire qui a été fait avant que les méthodes anonymes et les expressions lambda soient disponibles ou communes, et qui n'a pas été révisé depuis?

Le dernier. En réalité, la spécification C # 1.0 ne précisait pas si la variable de boucle était à l'intérieur ou à l'extérieur du corps de la boucle, car elle ne faisait aucune différence observable. Lorsque la sémantique de fermeture a été introduite dans C # 2.0, le choix a été fait de placer la variable de boucle à l'extérieur de la boucle, conformément à la boucle "pour".

Je pense qu'il est juste de dire que tous regrettent cette décision. C'est l'un des pires "pièges" de C #, et nous allons prendre le changement radical pour le réparer. En C # 5, la variable de boucle foreach sera logiquement à l'intérieur du corps de la boucle et, par conséquent, les fermetures recevront une nouvelle copie à chaque fois.

La boucle for ne sera pas modifiée et la modification ne sera pas "rétablie" dans les versions précédentes de C #. Vous devez donc continuer à faire attention lorsque vous utilisez cet idiome.

1368
Eric Lippert

Ce que vous demandez est couvert en détail par Eric Lippert dans son article de blog Variable de fermeture sur la boucle considérée comme nuisible et sa suite.

Pour moi, l'argument le plus convaincant est que le fait d'avoir une nouvelle variable à chaque itération serait incohérent avec la boucle de style for(;;). Vous attendriez-vous à avoir un nouveau int i à chaque itération de for (int i = 0; i < 10; i++)?

Le problème le plus courant avec ce comportement est de créer une variable de fermeture sur itération. Il existe une solution de contournement simple:

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

Mon billet de blog sur ce problème: Closure over foreach variable in C # .

182
Krizz

Ayant été mordu par cela, j’ai l’habitude d’inclure des variables définies localement dans la portée la plus profonde que j’utilise pour transférer toute fermeture. Dans votre exemple:

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

Je fais:

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

Une fois que vous avez cette habitude, vous pouvez l’éviter dans le cas très rare très que vous souhaitiez réellement lier aux étendues extérieures. Pour être honnête, je ne pense pas l'avoir jamais fait.

100
Godeke

En C # 5.0, ce problème est résolu et vous pouvez fermer les variables de boucle et obtenir les résultats que vous attendez.

La spécification de langue dit:

8.8.4 La déclaration foreach

(...)

Un énoncé foreach du formulaire

foreach (V v in x) embedded-statement

est ensuite étendu à:

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

(...)

Le placement de v dans la boucle while est important pour la manière dont il est capturé par toute fonction anonyme se produisant dans l'instruction embedded. Par exemple:

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

Si v était déclaré en dehors de la boucle while, il serait partagé entre toutes les itérations et sa valeur après la boucle for serait la valeur finale, 13, qui est ce que l'invocation de f imprimerait. Au lieu de cela, étant donné que chaque itération a sa propre variable v, celle capturée par f lors de la première itération continuera à contenir la valeur 7, qui sera imprimée. (Remarque: les versions antérieures de C # déclarées v en dehors de la boucle while.)

59
Paolo Moretti

À mon avis, c'est une question étrange. Il est bon de savoir comment fonctionne le compilateur, mais ce n'est que "bon à savoir".

Si vous écrivez du code qui dépend de l'algorithme du compilateur, c'est une mauvaise pratique. Et il est préférable de réécrire le code pour exclure cette dépendance.

C'est une bonne question pour un entretien d'embauche. Mais dans la vraie vie, je n’ai jamais eu de problèmes à résoudre en entrevue.

Les 90% de foreach utilisent pour traiter chaque élément de la collection (pas pour sélectionner ou calculer certaines valeurs). Parfois, vous devez calculer des valeurs dans la boucle, mais il n'est pas conseillé de créer une boucle BIG.

Il est préférable d'utiliser des expressions LINQ pour calculer les valeurs. Parce que lorsque vous calculez beaucoup de choses dans la boucle, après 2-3 mois, lorsque vous (ou toute autre personne) lirez ce code, la personne en question ne comprendra pas en quoi cela consiste ni comment il devrait fonctionner.

0
TemaTre