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

- TechDays Paris 2010 : Déploiement de nouvelles technologies – Retour d’expérience par l’informatique de Microsoft par Blog Technique de Romelard Fabrice le il y a 33 minutes

- TechDays Paris 2010 : Plan de migration vers SharePoint 2010 par Blog Technique de Romelard Fabrice le il y a 4 heures et 16 minutes

- TechDays Paris 2010 : La pleinière du second jour par Blog Technique de Romelard Fabrice le il y a 5 heures et 21 minutes

- Visual Studio 2010 and .NET Framework 4 Release Candidate now available par Matthieu MEZIL le il y a 8 heures et 27 minutes

- Création d’une base de donnée sous SQL Azure par Le Blog (Vert) d'Arnaud JUND le il y a 9 heures et 24 minutes

- TechDays Paris 2010 : Les Services d’applications dans SharePoint 2010 par Blog Technique de Romelard Fabrice le il y a 19 heures et 23 minutes

- TechDays Paris 2010 : La GED et SharePoint 2010 par Blog Technique de Romelard Fabrice le il y a 23 heures et 21 minutes

- TechDays Paris 2010 : SharePoint 2010 et Les réseaux sociaux par Blog Technique de Romelard Fabrice le 02-08-2010, 15:40

- TechDays Paris 2010 : SharePoint 2010 – Description et nouveautés par Blog Technique de Romelard Fabrice le 02-08-2010, 14:33

- TechDays Paris 2010 : Pleinière Lundi par Blog Technique de Romelard Fabrice le 02-08-2010, 14:30