Expressions Lambda et boucles ForEach

This post is available in english here.

Pour pouvoir améliorer les performances d'un serialiseur de données, et afin d'utiliser une petite extension que j'ai ajouté récemment dans la librairie Umbrella, je suis tombé sur un petit "effet de bord" intéressant lors de l'utilisation de la création d'expressions lambda dans une boucle foreach.

Prenons cet exemple de code très simple :

    var actionList = new List<Func<int>>();

    foreach (var value in Enumerable.Range(0, 10))
    {
       actionList.Add(() => value);
    }

    actionList.ForEach(func => Console.Write("{0} ", func()));


Qui donne en résultat ceci :

   9 9 9 9 9 9 9 9 9 9

Ce n'est, bien entendu, pas vraiment ce à quoi on s'attendait.

Les expressions lambdas ont la particularité de permettre l'utilisation des variables accessibles au moment de leur déclaration. Cela rend ces expression très intéressantes, mais pour bien les utiliser, il faut comprendre comment ces expression sont matérialisées par le compilateur.

Comme beaucoup de fonctionnalités de C#, comme using, foreach, les iterateurs ou bien lock, les lambdas sont du sucre syntaxique destinés à simplifier l'écriture de code bien souvent verbeux. Il est tout à fait possible de faire une expansion du mot clé en code C#.

Prenons le cas de ce morceau de code :

    int a = 0;
    Action action = () => Console.WriteLine(a);
    action();


L'expression lambda est "matérialisée" par le compilateur C# sous forme d'une "DisplayClass", qui lui permet de stocker le contenu de la variable locale "a" :

    [CompilerGenerated]
    private sealed class <>c__DisplayClass1
    {
       public int a;

       public void <Main>b__0()
       {
          Console.WriteLine(this.a);
       }
    }


On peut constater que les identifiants pour la classe générée ne sont pas valides en C#, mais le sont tout à fait du point de vue du CLR. On constate aussi que la variable locale utilisée lors de la déclaration de la lambda se trouve en tant que variable membre de la classe contenant le code de l'expression lambda. Le compilateur écrit donc ceci pour créer l'expression lambda :

    int a = 0;

    var display = new <>c__DisplayClass1();
    display.a = a;

    Action action = new Action(display.<Main>b__0);

    action();


La encore, ce n'est pas du code valide en C#.

Mais alors, que se passe-t-il donc dans le cas de notre foreach, pour que la contenu de la variable soit répété ?

Si l'on analyse le premier exemple de code généré par le compilateur avec Reflector, on ne constate rien de particulier avec le visualiseur C# :

    List<Func<int>> actionList = new List<Func<int>>();
    using (IEnumerator<int> CS$5$0000 = Enumerable.Range(0, 10).GetEnumerator())
    {
       while (CS$5$0000.MoveNext())
       {
          int value = CS$5$0000.Current;
          actionList.Add(delegate {
             return value;
          });
       }
    }


L'expression lamda est représentée sous la forme d'une méthode anonyme, qui est effectivement un synonyme de lambda, mais cela n'explique toujours pas le comportement.

Il faut en fait regarder l'IL généré pour comprendre le comportement, et voici une version en C# du code généré :

    List<Func<int>> actionList = new List<Func<int>>();
    using (IEnumerator<int> CS$5$0000 = Enumerable.Range(0, 10).GetEnumerator())
    {
       var myLambda = new <>c__DisplayClass4();

       while (CS$5$0000.MoveNext())
       {
          int value = CS$5$0000.Current;

          myLambda.value = value;

          actionList.Add(new Func<int>(myLambda.b_0));
       }
    }


On constate assez facilement le problème ici : L'instance de la classe contenant la lambda n'est créée qu'une seule fois, et réutilisée plusieurs fois en réassignant une nouvelle valeur à chaque itération. Cela explique le fait que le résultat de l'exécution de toutes les expressions lambda retournent toujours la dernière valeur énumérée, puisque elles ont toutes la même instance du type "DisplayClass".

Par contre, si l'on écrit le code original sous cette forme :

    foreach (var value in Enumerable.Range(0, 10))
    {
       int myValue = value;
       actionList.Add(() => myValue);
    }


Le problème ne se produit plus, et cette fois ci, chaque lambda a bien la "bonne" valeur.

Du point de vue du compilateur, la création d'une nouvelle instance de classe "conteneur" de lambda doit certainement être la conséquence de la création d'une nouvelle variable. Dans le cas d'un foreach ce ne semble pas être le cas, et la variable est considérée comme n'étant créée qu'une seule fois, puis réutilisée.

Du point de vue du compilateur donc, une boucle foreach est étendue de la manière suivante :

    using (var it = Enumerable.Range(0, 10).GetEnumerator())
    {
       int value;

       while (it.MoveNext())
       {
          value = it.Current;
          actionList2.Add(() => value);
       }
    }


C'est peut-être une question d'interprétation, mais je ne m'attendais pas tout à fait à ça...

Il faut donc porter un minimum d'attention à la manière dont on utilise les variables locales dans les expression lambdas, en fonction de leur endroit de déclaration.

J'expliquerais dans un prochain article où Umbrella m'a été utile et pourquoi j'ai eu à utiliser les expression lambda dans une boucle foreach.

Publié mercredi 19 novembre 2008 00:07 par jay
Classé sous , ,
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires


Les 10 derniers blogs postés

- SharePoint : Bug sur la gestion des permissions et la synchronisation Office par Blog Technique de Romelard Fabrice le 07-10-2014, 11:35

- SharePoint 2007 : La gestion des permissions pour les Workflows par Blog Technique de Romelard Fabrice le 07-08-2014, 11:27

- TypeMock: mock everything! par Fathi Bellahcene le 07-07-2014, 17:06

- Coding is like Read par Aurélien GALTIER le 07-01-2014, 15:30

- Mes vidéos autour des nouveautés VS 2013 par Fathi Bellahcene le 06-30-2014, 20:52

- Recherche un passionné .NET par Tkfé le 06-16-2014, 12:22

- [CodePlex] Projet KISS Workflow Foundation lancé par Blog de Jérémy Jeanson le 06-08-2014, 22:25

- Etes-vous yOS compatible ? (3/3) : la feuille de route par Le blog de Patrick [MVP SharePoint] le 06-06-2014, 00:30

- [MSDN] Utiliser l'approche Contract First avec Workflow Foundation 4.5 par Blog de Jérémy Jeanson le 06-05-2014, 21:19

- [ #ESPC14 ] TH10 Moving mountains with SharePoint ! par Le blog de Patrick [MVP SharePoint] le 06-01-2014, 11:30