tag(s) Tags: C#, Linq
lu 2411 fois
6 pages
Frédéric Mélantois
Comprendre les bases de Linq to objects
La meilleure façon de comprendre le fonctionnement de Linq est sans doute de réaliser soi-même un petit développement similaire en C# 2.0 et de le faire évoluer sous C# 3.0 en intégrant chaque nouveauté du langage.
Par Frédéric Mélantois publié le 21/10/2007 à 22:53
 
Notez bien qu'il est indispensable de bien avoir assimilé C# 2.0, en particulier les notions de génériques et de méthodes anonymes pour la compréhension de ce chapitre. Notre but est de construire l'équivalent d'un mini Linq en C# 2.0.
Pour notre démonstration, nous prendrons un exemple relativement simple : Nous souhaitons effectuer la moyenne des notes moyennes d'un ensemble d'élèves. Pour cela, il nous suffit de créer deux objets entités représentant l'élève et la note :

public class NoteENT

{

    private float _note;

    private DateTime _date;

 

    public float Note { get { return _note; } set { _note = value; } }

    public DateTime Date { get { return _date; } set { _date = value; } }

}

 

public class EleveENT

{

    private string _nom;

    private string _prenom;

    private IEnumerable<NoteENT> _notes;

 

    public string Nom { get { return _nom; } set { _nom = value; } }

    public string Prenom { get { return _prenom; } set { _prenom = value; } }

    public IEnumerable<NoteENT> Notes { get { return _notes; } set { _notes = value; } }

}

Nous manipulerons une liste d'élèves, sur laquelle nous effectuerons des requêtes. Afin de ne pas alourdir notre exemple, nous nous contenterons de la liste générique du Framework 2.0, à savoir List<T>:

List<EleveENT> l = new List<EleveENT>();

EleveENT el1 = new EleveENT();

el1.Nom = "Martin";

el1.Prenom = "Michel";

List<NoteENT> nos1 = new List<NoteENT>();

NoteENT no1 = new NoteENT();

no1.Note = 3;

no1.Date = new DateTime(2006, 12, 12);

NoteENT no2 = new NoteENT();

no2.Note = 9.5f;

no2.Date = new DateTime(2007, 4, 1);

nos1.Add(no1);

nos1.Add(no2);

el1.Notes = nos1;

 

EleveENT el2 = new EleveENT();

el2.Nom = "Durand";

el2.Prenom = "Cyril";

List<NoteENT> nos2 = new List<NoteENT>();

NoteENT no3 = new NoteENT();

no3.Note = 17;

no3.Date = new DateTime(2006, 12, 12);

NoteENT no4 = new NoteENT();

no4.Note = 15;

no4.Date = new DateTime(2007, 4, 1);

nos2.Add(no3);

nos2.Add(no4);

el2.Notes = nos2;

 

l.Add(el1);

l.Add(el2);

Il est nécessaire de créer une classe permettant de manipuler une liste d'objets tels que des élèves ou des notes. Nous souhaitons pouvoir sélectionner des critères (le nom de l'élève, ou l'ensemble de ses notes, ou bien une note, etc.). Pour créer cette fonctionnalité, il faut créer une méthode de sélection prenant en argument une « List<T> » mais ce serait oublier l'implémentation de l'interface IEnumerable<T> qui est en mesure de nous offrir une plus grande souplesse d'emploi. Un deuxième argument à notre méthode est indispensable pour préciser le critère que l'on souhaite sélectionner. L'usage d'un délégué jouera le rôle d'un pointeur de fonction dans laquelle nous pourrons préciser ce critère. Ce délégué prendra donc en charge une source à partir de laquelle nous produirons un résultat.

public delegate TResult Func<TSource, TResult>(TSource source);

 

public static class ComplementEnumerable

{

   public static IEnumerable<TResult> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector)

   {

        foreach (TSource s in source)

 

            yield return selector.Invoke(s);

    }

}

L'emploi d'une méthode anonyme en argument de la méthode « Select » plutôt qu'une référence à une méthode dont la définition serait figée concentre en une seule ligne de code les éléments sélectionnés. Ceci apporte une meilleure lecture et favorise sans doute d'éventuels changements. Testons la méthode précédente :

IEnumerable<IEnumerable<NoteENT>> t = ComplementEnumerable.Select<EleveENT, IEnumerable<NoteENT>>(l,

                                                            delegate(EleveENT c) { return c.Notes; });

L'élément retourné par la méthode anonyme est parfaitement visible, il s'agit de la propriété « Notes » de l'objet « EleveENT », il s'agit donc bien d'une sélection.
Si on souhaite affiner celle-ci en récupérant tous les sous-ensembles de notes des élèves, il suffit de sélectionner la propriété Note de l'objet « NoteENT » :

IEnumerable<IEnumerable<float>> t = ComplementEnumerable.Select<EleveENT, IEnumerable<float>>(l,

            delegate(EleveENT c)

            {

                return ComplementEnumerable.Select<NoteENT, float>(c.Notes, delegate(NoteENT n)

                { return n.Note; }

                );

            });   

Le résultat renvoie bien tous les sous-ensembles des notes obtenues par l'ensemble des élèves. La première remarque qui peut venir à l'esprit, est la lourdeur de la syntaxe, même si le résultat répond parfaitement à notre attente.
Cherchons à ajouter d'autres fonctionnalités à la classe « ComplementEnumerable ». Généralement, lorsque vous réalisez une sélection, vous pouvez fixer des conditions. Ces dernières doivent se vérifier. L'ajout d'une méthode « Where » pouvant prendre en argument une méthode anonyme renvoyant un booléen est parfaitement adapté à la prise en compte de conditions :

public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> selector)

{

    foreach (TSource s in source)

        if (selector.Invoke(s))

            yield return s;

}

Le type du premier argument de la méthode est du même type que celui retourné par celle-ci. Elle ne renvoie que les éléments répondant aux conditions fixées.
L'utilisation de la clause « Where » peut se faire par exemple comme suit :

IEnumerable<IEnumerable<float>> t = ComplementEnumerable.Select<EleveENT, IEnumerable<float>>(l,

    delegate(EleveENT c)

            {

                return ComplementEnumerable.Select<NoteENT, float>(ComplementEnumerable.Where<NoteENT>(c.Notes,

                    delegate(NoteENT n)

                    { return n.Date >= new DateTime(2007,4,1); }

                    ), delegate(NoteENT n)

                { return n.Note; }

                );

            });

La requête ci-dessus permet de récolter l'ensemble des groupes de notes attribuées après le 1er Avril 2007. La syntaxe s'alourdit mais nous parvenons toujours à nos volontés.
A l'origine, nous souhaitions obtenir une moyenne. Il nous faut donc développer cette fonctionnalité. Complétons notre classe « ComplementEnumerable » par une méthode nommée « Average » permettant d'obtenir la moyenne des notes :

public static float Average(IEnumerable<float> source)

{

    double num1 = 0;

    long num2 = 0;

    foreach (float num3 in source)

    {

        num1 += num3;

        num2++;

    }

    return (float)(num1 / (double) num2);

}

Utilisons cette nouvelle méthode en calculant la moyenne des notes moyennes de l'ensemble des élèves :

float r = ComplementEnumerable.Average(ComplementEnumerable.Select<EleveENT, float>(l, delegate(EleveENT c)

      {

      return ComplementEnumerable.Average(ComplementEnumerable.Select<NoteENT, float>(c.Notes, delegate(NoteENT n)

            { return n.Note; }

          ));

      }

      ));

Nous pouvons alléger cette syntaxe en ajoutant une autre définition pour la méthode « Average » :

public static float Average<TSource>(IEnumerable<TSource> Source, Func<TSource, float> selector)

{

    return ComplementEnumerable.Average(ComplementEnumerable.Select<TSource, float>(Source, selector));

}

Cette définition utilise astucieusement la combinaison d'une sélection et d'une moyenne. Grâce à cet ajout de méthode dans la classe « ComplementEnumerable » la syntaxe de notre "mini-Linq" s'en trouve alors légèrement simplifiée dans un premier temps :

float r = ComplementEnumerable.Average(ComplementEnumerable.Select<EleveENT, float>(l, delegate(EleveENT c)

            {

                return ComplementEnumerable.Average<NoteENT>(c.Notes, delegate(NoteENT n)

                    { return n.Note; }

                );

            }

            ));

Et si nous poussons la simplification complètement :

float r = ComplementEnumerable.Average<EleveENT>(l, delegate(EleveENT c)

    {

        return ComplementEnumerable.Average<NoteENT>(c.Notes, delegate(NoteENT n)

            { return n.Note; }

        );

    }

    );

Avouez que si la syntaxe est parfois lourde, la classe « ComplementEnumerable » répond parfaitement à notre attente. Veuillez bien noter que nous avons effectué tout cela uniquement avec Visual Studio 2005 et C# 2.0 ! L'objectif avoué dans les prochains chapitres est de reprendre intégralement le code de notre « Mini-Linq » sous Visual Studio 2008 et de le faire évoluer avec la version 3.0 du compilateur C#.
S'il s'agit de votre première lecture, vous pouvez passer au chapitre suivant consacré à la déduction de type et à l'utilisation des expressions Lambda.
L'objet de ce paragraphe est de développer le sujet, de poser des questions, d'inciter à la découverte d'éléments un peu plus avancés.

Lorsque vous ferez évoluer votre code C# 2.0 en C# 3.0, vous aurez tendance à intégrer les nouveautés du langage. Par exemple, la syntaxe de définition des objets entités «Elève » et « Note » évoluerait en C# 3.0 comme suit :

public class NoteENT

{

    public float Note { get; set; }

    public DateTime Date { get; set;}

}

 

public class EleveENT

{

    public string Nom { get; set; }

    public string Prenom { get; set; }

    public IEnumerable<NoteENT> Notes { get; set; }

}

J'ai montré dans mon précédent article « C# 3.0 Beta, déclarations et initialisations simplifiées, regardons sous le capot ! » que cet allègement syntaxique n'avait aucune conséquence sur la performance. Par contre, l'emploi des initialiseurs d'objets, commodités offertes par C# 3.0, peut susciter quelques interrogations. En effet, si nous modifions la façon d'initialiser les propriétés des objets « Elève » et « Note » comme suit :

List<EleveENT> l = new List<EleveENT> { new EleveENT {

                    Nom = "Martin",

                    Prenom = "Michel",

                    Notes = new List<NoteENT> {

                                new NoteENT {

                                    Note = 3,

                                    Date = new DateTime(2006,12,12) },

                                    new NoteENT {

                                        Note=9.5f,

                                        Date=new DateTime(2007,4,1)}}}

                ,

                new EleveENT {

                    Nom = "Durand",

                    Prenom = "Cyril",

                    Notes = new List<NoteENT> {

                                new NoteENT {

                                    Note = 17,

                                    Date = new DateTime(2006,12,12) },

                                    new NoteENT {

                                        Note=15,

                                        Date= new DateTime(2007,4,1)}}

 

}};

L'analyse du code IL (Intermediate Language) montre l'usage de variables intermédiaires. Pour un emploi raisonné des initialiseurs d'objets, je vous invite vivement à la lecture de l'article précédemment cité, accompagné des commentaires pertinents des lecteurs.

Ayez bien à l'esprit que vous ne pouvez pas vous fier au code C# délivré par l'utilitaire Reflector en lecture d'un assemblage. En effet, cet utilitaire ne fait qu'une lecture des octets de l'assembly afin de proposer une retranscription dans un langage de haut niveau tel que C# ou Visual Basic. Seul l'IL (intermediate language) est complètement fiable puisque ce dernier se base sur la lecture des OPCODES (codés sur un ou deux octets en fonction des instructions). Vous pouvez vérifier ces dires en compilant le code écrit plus haut (voir « Piste verte ») sous Visual Studio 2005 (référence au Framework 2.0) d'une part, puis en compilant ce même code sous Visual Studio 2008 (Référence au Framework 3.5). Ayant obtenu deux assemblies ne se basant pas sur les mêmes versions du Framework .NET, vous les soumettrez à l'interprétation de l'utilitaire Reflector. Si vous comparez le code IL, vous constaterez qu'il est identique. Par contre, le code C# proposé sera différent. C'est donc que l'utilitaire génère le code C# en fonction des versions du Framework .NET.

Lorsque j'ai écrit initialement le code de la méthode « Average », celui-ci était différent du code présenté plus haut :

public static float Average(IEnumerable<float> source)

{

    double num1 = 0;

    double num2 = 0;

    foreach (float num3 in source)

    {

        num1 += num3;

        num2++;

    }

    return (float)(num1 / num2);

}

J'étais passé complètement à côté d'une optimisation que l'on peut voir à partir de l'analyse du code natif pouvant être généré. Sachez que celle-ci n'a pas été oubliée par l'équipe en charge de développer le Framework Linq to Objects. Vous pouvez étudier le code de la méthode « Average » de la classe statique « Enumerable » de l'espace de nom « System.Linq » en désassemblant l'assembly « System.Core » via Reflector. Il est donc préférable d'écrire :

public static float Average(IEnumerable<float> source)

{

    double num1 = 0;

    long num2 = 0;

    foreach (float num3 in source)

    {

        num1 += num3;

        num2++;

    }

    return (float)(num1 / (double) num2);

}

Je ne vous ferai pas la démonstration en reproduisant ici le code natif dans les deux cas, mais je vous invite à les comparer pour bien comprendre où se trouve le gain de performance. L'utilitaire « Cordbg » devrait vous y aider. Retenez qu'incrémenter une variable de type « double » est plus coûteux qu'incrémenter une variable de type « long ».

L'exploration de la classe « Enumerable » de « System.Linq », en particulier les méthodes « Select », « Where » et « Average », met en évidence une gestion d'erreurs. Les créateurs ont pensé à renvoyer une erreur qualifiée dans le cas où les paramètres passés à la méthode seraient nuls. Une classe statique « Error » de portée « internal » a même été écrite à cet effet. C'est un traitement que je n'ai pas effectué, comme me l'a très justement souligné Matthieu Mezil lors de la relecture d'une des pré-versions de cet article. Vous pouvez bien évidemment le réaliser et même changer le comportement de Linq par les « surcharges » d'extension de méthodes que vous découvrirez un peu plus loin dans cet article. Les arguments nuls pourraient être traités différemment :

public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> selector)

{

    if (source == null || selector == null)

        return source;

 

        foreach (TSource s in source)

            if (selector.Invoke(s))

                yield return s;

}

Le lecteur curieux, n'ayant pas encore mené de telles investigations, pourra se demander ce qui se cache derrière les mots clés « yield return ». Il utilisera l'utilitaire Reflector pour regarder sous le capot. Il retrouvera une structure qu'il connaît bien et comprendra la magie du "retour dans la méthode" évoqué lors des différentes présentations du langage de haut niveau C# 2.0. Il faut bien comprendre que ce « retour dans la méthode » n'a pas de réalité au niveau de l'IL. Le compilateur JIT (Just in Time) de la plateforme crée un contexte (une pile, allocation d'une zone mémoire pour les variables locales) avant l'exécution des instructions de la méthode. Quand celle-ci prend fin, le contexte n'est pas maintenu. Ce qui se cache derrière cette magie n'est qu'un objet maintenu en mémoire. Il s'agit d'une classe privée non dérivable, implémentant les interfaces « IEnumerable<TResult> », « IEnumerable », « IEnumerator<TResult> », « IEnumerator », « IDisposable », générée par le compilateur C# (que sa version soit 2.0 ou 3.0). Je vous invite à décortiquer sous Reflector les méthodes « Select » et « Where » de la classe « ComplementEnumerable » que nous avons construite. Vous pourrez ainsi constater que toute la logique conditionnelle contenue à l'intérieur du « foreach » de l'élément « IEnumerable<TSource> » se trouve retranscrite dans la méthode « MoveNext ».

 Commentaire - Comprendre les bases de Linq to objects 

Discussion démarée par Matthieu Mezil le 22/10/2007 à 11:37 , 1 commentaire(s).

 Dernières Publications      

Windows Media Center et WCF : développez votre maison intelligente
  Le développement d'applications pour Windows Media Center est facilité avec l'arrivée du SDK 5.3. Même si l'on sent un modèle objet bien lourd derrière, il devient plus facile d'exposer les fonctionnalités de WMC sous la forme de services WCF.
par Frédéric Colin posté le 23/06/2008 à 08:04, lu 295 fois, #0
Notions avancées avec Biztalk Server 2006 R2
  Utilisation des notions d'interchange, corrélation et convoi avec BizTalk Server 2006 R2
par Kader Yildirim posté le 09/06/2008 à 08:04, lu 288 fois, #0
Lucene Persistence Engine pour Evaluant Universal Storage Services
  Suite à l'article de Laurent Kempé, voici un moteur de stockage pour EUSS permettant l'indexation d'entités métier avec Lucene.
par Nicolas Penin posté le 01/06/2008 à 23:38, lu 510 fois, #1
Tags: C#, Linq
XMLA Trivia : Découverte du XMLA
  Le XMLA (XML for Analysis) est un langage normalisé par plusieurs éditeurs BI pour simplifier l'accès aux données aux cubes et aux métadonnées des bases multidimensionnelles.
par Renaud Harduin posté le 25/05/2008 à 11:57, lu 523 fois, #0
Exploiter les données CSV via Linq en toute simplicité
  A partir du requêteur dynamique fourni en exemple avec Visual Studio 2008, nous allons essayer de remplir les propriétés d'un ensemble d'objets à partir des données d'un fichier CSV. Nous enrichirons aussi le parseur de nos propres fonctions.
par Frédéric Mélantois posté le 17/05/2008 à 11:41, lu 1713 fois, #0
Comment manipuler simplement le contenu d'un fichier WordML ?
  Manipulations autour du format WordML
par Fabien Reinle posté le 14/05/2008 à 23:55, lu 781 fois, #0
Polymorphisme et contrats de données WCF
  WCF aborde les types polymorphes du point de vue de la sérialisation. En effet, la connaissance du type réel potentiel est rendue nécessaire dès la description du contrat de données. Une fois n'est pas coutume, j'ai réalisé l'exemple en VB.NET.
par Frédéric Colin posté le 14/05/2008 à 08:40, lu 1881 fois, #2
A la découverte de BizTalk Server 2006 3/3
  Développer un assembleur pour BizTalk Server 2006 R2
par Kader Yildirim posté le 06/05/2008 à 13:20, lu 511 fois, #0

 Dernières Actualités      

Deep Earth – Une belle utilisation de Virtual Earth et de Silverlight Deep Zoom
  Ce projet très intéressant est disponible sur Codeplex et vous pouvez voir une démo sur la page suivante . Bien entendu comme touts les projets sur Codeplex vous avez accès aux sources....
Tags: Silverlight
Sortie de JetBrains ReSharper 4.0 en version finale, l’outil ultime pour Visual Studio
  Après plusieurs mois de Early Access Program (EAP) , JetBrains met enfin à disposition la version finale de son outil ReSharper 4.0 . Cette nouvelle version est disponible pour Visual Studio 2005 &...
Tags: Visual Studio 2008, Visual Studio 2005, Outils
BoutDuTunnel v1.4
  BoutDuTunnel est un petit logiciel de tunneling réseau écrit en C#. Il permet par exemple d’accéder aux services ftp/smtp/pop/telnet/nntp/… sur des réseaux qui n’autorisent...
BI Framework & sample sur CodePlex
  Après plusieurs demandes, je me suis décidé à déposer l'ensemble des sources et du BI Framework MS proposés dans mes articles sur codeplex : http://www.codeplex.com/BILAB Je le mettrais à jour au fil des...
Injection de code et API de profiling .NET
  Si vous êtes intéressés par la sécurité du Framework, par le reverse engineering et la manipulation/injection de code .NET et les packers, alors jetez un coup d’œil...
NDepend pour l'analyse statique de code .NET
  Pour ceux qui ne connaissent pas NDepend , il s’agit d’un outil d’analyse statique de code .NET qui permet de remonter des informations à toute une équipe de développement. NDepend aide à travailler sur...
Tags: Outils