1865 votes

Est-il une raison pour C#'s la réutilisation de la variable dans un foreach?

Lors de l'utilisation des expressions lambda, d'anonymes ou de méthodes en C#, il faut se méfier de l' accès à l'modifié fermeture piège. Par exemple:

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

En raison de la modification de la fermeture, le code ci-dessus affichera tous les de la Where des clauses sur la requête soit basée sur la valeur finale de l' s.

Comme expliqué ici, c'est parce que l' s variable déclarée en foreach boucle ci-dessus est traduit comme ceci dans le compilateur:

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

au lieu de cela:

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

Comme l'a souligné ici, il n'y a pas les avantages de performance à la déclaration d'une variable en dehors de la boucle, et dans des circonstances normales, la seule raison pour laquelle je peux penser pour ce faire 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 boucle ne peut pas être utilisé 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 dans une manière qui le rend très sujettes à une erreur qui est souvent difficile de trouver et de débogage, tout en ne produisant aucun perceptible avantages.

Est-il quelque chose que vous pouvez faire avec foreach boucles de cette façon qu'on ne pouvait pas s'ils sont compilés avec un intérieur d'étendue variable, ou est-ce juste un choix arbitraire qui a été fait avant les méthodes anonymes et les expressions lambda étaient disponibles ou de la commune, et qui n'a pas été révisée depuis lors?

1523voto

Eric Lippert Points 300275

Le compilateur déclare la variable dans une manière qui le rend très sujettes à une erreur qui est souvent difficile de trouver et de débogage, tout en ne produisant aucun perceptible avantages.

Votre critique est tout à fait justifiée.

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

La fermeture de plus de la variable de boucle considéré comme nocif

Est-il quelque chose que vous pouvez faire avec les boucles foreach de cette façon qu'on ne pouvait pas s'ils sont compilés avec un intérieur dont l'étendue est variable? ou est-ce juste un choix arbitraire qui a été fait avant les méthodes anonymes et les expressions lambda étaient disponibles ou de la commune, et qui n'a pas été révisée depuis lors?

Le dernier. Le C# 1.0 de la spécification en réalité n'a pas dit si la variable de boucle est à l'intérieur ou à l'extérieur du corps de la boucle, comme il ne fait aucune différence observable. Lors de la fermeture de la sémantique ont été introduites en C# 2.0, le choix a été fait de mettre la variable de boucle à l'extérieur de la boucle, en cohérence avec la boucle "for".

Je pense qu'il est juste de dire que tous le regretter. C'est l'une des pires erreurs en C#, et nous allons prendre de la modification de rupture pour le fixer. En C# 5 la boucle foreach variable sera logiquement à l'intérieur du corps de la boucle, et donc de fermetures permettront d'obtenir une nouvelle copie de tous les temps.

L' for boucle ne sera pas changé, et que le changement ne sera pas en arrière "porté" pour les précédentes versions de C#. Vous devez donc continuer à être prudent lors de l'utilisation de cet idiome.

207voto

Krizz Points 7273

Ce que vous demandez est largement couvert par Eric Lippert dans son billet de blog de Clôture sur la variable de boucle considéré comme nocif et sa suite.

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

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

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 à propos de ce problème: Fermeture foreach variable en C#.

117voto

Godeke Points 10401

Ayant été mordu par la présente, j'ai l'habitude de y compris au niveau local des variables définies à l'intérieur de la portée que j'utilise pour le transfert à une 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 les très rares cas où vous avez réellement l'intention de se lier à l'extérieur étendues. Pour être honnête, je ne pense pas que j'ai jamais fait.

76voto

Paolo Moretti Points 9519

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

La spécification du langage dit:

8.8.4 L'instruction foreach

(...)

Une instruction foreach de la forme

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

(...)

L'emplacement de v à l'intérieur de la boucle while est important pour la façon dont il est capturé par une fonction anonyme survenant dans le embedded-déclaration. 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ée en dehors de la boucle while, il serait partagée parmi toutes les itérations, et de sa valeur après la boucle serait la la valeur finale, 13, ce qui est ce que l'invocation de l' f serait d'imprimer. Au lieu de cela, parce que chaque itération dispose de sa propre variable v, l'un capturé par f dans la première itération continuera de tenir la valeur 7, qui est ce qui va être imprimé. (Remarque: les versions antérieures de C# déclarée v en dehors de la boucle while.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X