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


Lorsque vous utilisez des expressions lambda ou des méthodes anonymes en C #, nous devons nous méfier des accès à la fermeture modifiée piège. 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 provoquera tous les Where clauses sur la requête à être basée sur la valeur finale de s.

Comme expliqué ici, cela arrive parce que le s variable déclarée dans foreach La boucle ci-dessus est traduite comme ceci dans le compilateur:

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

au lieu de comme ça:

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

Comme indiqué ici, il n'y a aucun avantage de performance à déclarer une variable en dehors de la boucle, et dans des circonstances normales, la seule raison que je peux penser pour faire cela est si vous prévoyez 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 un foreach La boucle ne peut pas être utilisée en dehors de la boucle:

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

Ainsi, le compilateur déclare la variable d'une manière qui la rend très vulnérable à une erreur souvent difficile à trouver et à déboguer, tout en ne produisant aucun bénéfice perceptible.

Y at-il quelque chose que vous pouvez faire avec foreach boucle de cette façon que vous ne pourriez pas si elles ont été compilées avec une variable interne, ou est-ce juste un choix arbitraire qui a été fait avant que les méthodes anonymes et expressions lambda étaient disponibles ou communes, et qui n'a pas été révisé depuis?


1482
2018-01-17 17:21


origine


Réponses:


Le compilateur déclare la variable d'une manière qui la rend très vulnérable à 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:

Fermeture sur la variable de boucle considérée comme nuisible

Y a-t-il quelque chose que vous pouvez faire avec les boucles foreach de cette façon que vous ne pourriez pas faire si elles étaient compilées avec une variable 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'ont pas été révisées depuis?

Le dernier. 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 en dehors de la boucle, en accord avec la boucle "for".

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

le for La boucle ne sera pas modifiée et la modification ne sera pas "reportée" sur les versions précédentes de C #. Vous devriez donc continuer à faire attention en utilisant cet idiome.


1279
2018-01-17 17:56



Ce que vous demandez est entièrement couvert par Eric Lippert dans son blog Fermeture sur la variable de boucle considérée comme nuisible et sa suite.

Pour moi, l'argument le plus convaincant est qu'avoir une nouvelle variable dans chaque itération serait incompatible avec for(;;)boucle de style. Est-ce que vous vous attendez à avoir une nouvelle int i dans chaque itération de for (int i = 0; i < 10; i++)?

Le problème le plus commun avec ce comportement est de faire une fermeture sur variable d'itération et il a une solution de contournement facile:

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

Mon blog sur ce problème: Fermeture sur une variable foreach en C #.


174
2018-01-17 17:39



Ayant été mordu par cela, j'ai l'habitude d'inclure des variables définies localement dans la portée la plus interne que j'utilise pour transférer à n'importe quelle 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 très cas rare vous avez réellement l'intention de se lier aux périmètres extérieurs. Pour être honnête, je ne pense pas l'avoir jamais fait.


95
2018-01-17 17:47



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

La spécification de la langue dit:

8.8.4 La déclaration foreach

(...)

Une déclaration 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 à l'intérieur de la boucle while est important pour la façon dont il est   capturé par toute fonction anonyme se produisant dans le   instruction intégrée. 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 a été déclaré en dehors de la boucle while, il serait partagé   parmi toutes les itérations, et sa valeur après la boucle for serait le   valeur finale, 13, qui est ce que l'invocation de f imprimerait.   Au lieu de cela, parce que chaque itération a sa propre variable v, celui   capturé par f dans la première itération continuera à détenir la valeur    7, qui est ce qui sera imprimé. (Note: les versions antérieures de C #   déclaré v en dehors de la boucle while.)


52
2017-09-03 13:58



À 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 pas rencontré de problèmes que j'ai résolus en entretien d'embauche.

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

Il est préférable d'utiliser des expressions LINQ pour calculer les valeurs. Parce que quand vous calculez beaucoup de chose à l'intérieur de la boucle, après 2-3 mois quand vous (ou quelqu'un d'autre) lira ce code personne ne comprendra pas ce que c'est et comment cela devrait fonctionner.


0
2018-06-15 07:31