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




boucle while (5)

Lorsque vous utilisez des expressions lambda ou des méthodes anonymes en C #, nous devons nous méfier de l' 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 entraînera toutes les clauses Where sur la requête sur la valeur finale de s .

Comme expliqué here , cela se produit parce que la variable s déclarée dans la boucle foreach 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é here , 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 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.

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 les boucles foreach 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 avant que les méthodes anonymes et les expressions lambda soient disponibles ou communes? n'a pas été révisé depuis?


Answers

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 des méthodes anonymes et des 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 du corps de la boucle, et donc les fermetures recevront une nouvelle copie à chaque fois.

La boucle for 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.


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 dans la boucle while est important pour la façon dont il est capturé par toute fonction anonyme apparaissant dans l'instruction incorporé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 é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 correspond à l'impression de l'invocation de f . Au lieu de cela, parce que chaque itération possède sa propre variable v , celle capturée par f dans la première itération contiendra la valeur 7 , qui sera imprimée. ( Remarque: les versions antérieures de C # ont été déclarées v dehors de la boucle while. )


Ce que vous demandez est bien 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 la boucle de style for(;;) . Souhaitez-vous avoir un nouvel 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 article de blog à propos de ce problème: Fermeture de la variable foreach en C # .


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 cas très rare que vous avez réellement l'intention de lier aux portées externes. Pour être honnête, je ne pense pas l'avoir jamais fait.


  • Variable non levante

    let ne portera pas toute la portée du bloc dans lequel il apparaît. En revanche, var pourrait hisser comme ci-dessous.

    {
       console.log(cc); // undefined. Caused by hoisting
       var cc = 23;
    }
    
    {
       console.log(bb); // ReferenceError: bb is not defined
       let bb = 23;
    }
    

    En fait, Per @Bergi, var et let sont levés .

  • Collecte des ordures

    La portée de bloc de let est utile pour les fermetures et la récupération de place pour récupérer la mémoire. Considérer,

    function process(data) {
        //...
    }
    
    var hugeData = { .. };
    
    process(hugeData);
    
    var btn = document.getElementById("mybutton");
    btn.addEventListener( "click", function click(evt){
        //....
    });
    

    Le rappel du gestionnaire de click n'a pas du tout besoin de la variable hugeData . Théoriquement, après le process(..) , l’énorme structure de données hugeData pourrait être récupérée. Cependant, il est possible que certains moteurs JS doivent toujours conserver cette structure énorme, car la fonction click a une fermeture sur toute l'étendue.

    Cependant, la portée du bloc peut transformer cette énorme structure de données en déchets.

    function process(data) {
        //...
    }
    
    { // anything declared inside this block can be garbage collected
        let hugeData = { .. };
        process(hugeData);
    }
    
    var btn = document.getElementById("mybutton");
    btn.addEventListener( "click", function click(evt){
        //....
    });
    
  • let boucles

    let in the loop peut la relier à chaque itération de la boucle, en s'assurant de lui réaffecter la valeur à partir de la fin de l'itération de boucle précédente. Considérer,

    // print '5' 5 times
    for (var i = 0; i < 5; ++i) {
        setTimeout(function () {
            console.log(i);
        }, 1000);  
    }
    

    Cependant, remplacez var par let

    // print 1, 2, 3, 4, 5. now
    for (let i = 0; i < 5; ++i) {
        setTimeout(function () {
            console.log(i);
        }, 1000);  
    }
    

    Parce que let créer un nouvel environnement lexical avec ces noms pour a) l’expression d’initialisation b) chaque itération (avant l’évaluation de l’expression d’incrémentation), vous trouverez plus de détails here .





c# foreach lambda scope anonymous-methods