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
 
Avant d'aborder pleinement le sujet, je vous suggère de mettre en commentaires la définition du délégué « Func ». En effet, cette dernière n'est plus utile car le Framework 3.5 fournit de base ce délégué dans l'espace de nom « System » . Les utilisateurs des premières CTP d'Orcas et de la Beta 1 auront remarqué la migration du délégué « Func » fourni par l'assembly « System.Core », du namespace "System.Linq" vers le namespace "System" à partir de la Beta 2 du Framework.
Pour notre exposé, rappelons le code de notre classe statique « ComplementEnumerable » :

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);

    }

 

    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);

    }

 

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

    {

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

    }

 

    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;

    }

}

Il faut bien avouer que même avec une déduction de types fournie par le compilateur de C# 3.0, comme nous l'avons vue dans le chapitre précédent, l'utilisation de notre classe n'est pas très aisée. En effet, lorsqu'on effectue une requête un peu plus complexe qu'une simple sélection, chaque méthode utilisée dans celle-ci prend en paramètre le résultat d'une autre. Dans notre classe statique, on peut constater que chacune des méthodes prend en paramètre un « IEnumerable<T> ». Si seulement nous pouvions modifier les sources de tout objet implémetant « IEnumerable<T> », nous n'hésiterions pas une seconde à faire une extension des méthodes pour rajouter les « Select », « Where » etc... C'est à partir de cette constatation que l'équipe en charge de C# 3.0 a mis au point ce que l'on appelle les « méthodes d'extension ».
Pour créer une méthode d'extension, il suffit de créer une classe statique dont le nom importe peu avec des méthodes statiques concernant les objets que l'on souhaite « étendre ». Ces derniers sont désignés par le mot clé « this », en premier paramètre de la méthode.
Si nous appliquons cette nouveauté à notre classe « ComplementEnumerable », la définition de celle-ci devient :

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

 

public static class ComplementEnumerable

{

    public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source,

        Func<TSource, TResult> selector)

    {

        foreach (TSource s in source)

 

            yield return selector.Invoke(s);

    }

 

    public static float Average(this IEnumerable<float> source)

    {

        double num1 = 0;

        long num2 = 0;

        foreach (float num3 in source)

        {

            num1 += num3;

            num2++;

        }

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

    }

 

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

    {

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

    }

 

    public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,

        Func<TSource, bool> selector)

    {

        foreach (TSource s in source)

            if (selector.Invoke(s))

                yield return s;

    }

}

Essayons de mettre en pratique cette nouveauté à travers une requête permettant d'obtenir la liste des ensembles de notes obtenues par des élèves après une date donnée. Rappelons ce que serait le code de cette requête si nous étions privés du confort apporté par les méthodes d'extension :

var r = ComplementEnumerable.Select(l, c =>

                ComplementEnumerable.Select(ComplementEnumerable.Where(c.Notes, n =>

                n.Date >= new DateTime(2007,4,1)), f => f.Note)

            );

En appliquant les méthodes d'extension, le code de la requête se trouve extrêmement simplifié :

var h = l.Select(c => c.Notes.Where(n => n.Date >= new DateTime(2007,4,1)).Select(f => f.Note));

Rappelons à titre de comparaison, le code que nous avions écrit pour effectuer la même requête dans le premier chapitre consacré à la création d'un « mini-Linq » en C# 2.0 :

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

                );

            });

Le langage C# 3.0 nous a donc permis de réduire considérablement le code nécessaire pour exprimer une même requête. A chaque intégration d'une nouveauté du langage, nous avons montré que « sous le capot » après désassemblage de l'assembly, l'IL (intermediate Language) était parfaitement identique. Sachez qu'il en est de même avec l'utilisation des méthodes d'extension.
Afin de vérifier que l'utilisation des méthodes d'extension n'occassionne pas de changement sous le capot, Il suffit de comparer le résultat des désassemblages des différentes assemblies, celui correspondant au chapitre « Un mini-Linq en C# 2.0 » et celui de ce chapitre. Pour cela, vous pouvez utiliser l'utilitaire « ILDASM » ou « Reflector ». Si on extrait l'IL correspondant à la requête vue quelques lignes plus haut, vous constaterez qu'il est identique dans les deux cas :

L_0190: ldsfld class [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>

        ConsoleApplication3.Program::<>9__CachedAnonymousMethodDelegate13

L_0195: brtrue.s L_01aa

L_0197: ldnull

L_0198: ldftn class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>

    ConsoleApplication3.Program::<Main>b__b(class ConsoleApplication3.Program/EleveENT)

L_019e: newobj instance void [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>::.ctor(object, native int)

L_01a3: stsfld class [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>

        ConsoleApplication3.Program::<>9__CachedAnonymousMethodDelegate13

L_01a8: br.s L_01aa

L_01aa: ldsfld class [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>

        ConsoleApplication3.Program::<>9__CachedAnonymousMethodDelegate13

L_01af: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!1>

    ConsoleApplication3.ComplementEnumerable::Select<class ConsoleApplication3.Program/EleveENT,

           class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>(

                class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,

                       class [System.Core]System.Func`2<!!0, !!1>)

L_01b4: stloc.3

L_01b5: ldloc.0

L_01b6: ldsfld class [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>

        ConsoleApplication3.Program::<>9__CachedAnonymousMethodDelegate14

L_01bb: brtrue.s L_01d0

L_01bd: ldnull

L_01be: ldftn class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>

    ConsoleApplication3.Program::<Main>b__e(class ConsoleApplication3.Program/EleveENT)

L_01c4: newobj instance void [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>::.ctor(object, native int)

L_01c9: stsfld class [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>

        ConsoleApplication3.Program::<>9__CachedAnonymousMethodDelegate14

L_01ce: br.s L_01d0

L_01d0: ldsfld class [System.Core]System.Func`2<class ConsoleApplication3.Program/EleveENT,

    class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>

        ConsoleApplication3.Program::<>9__CachedAnonymousMethodDelegate14

L_01d5: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!1>

    ConsoleApplication3.ComplementEnumerable::Select<class ConsoleApplication3.Program/EleveENT,

        class [mscorlib]System.Collections.Generic.IEnumerable`1<float32>>(

            class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,

                    class [System.Core]System.Func`2<!!0, !!1>)

L_01da: stloc.s h

Seuls les noms des méthodes anonymes changent. Outre la requête, il convient de s'intéresser aussi à la classe statique « ComplementEnumerable », que ce soit à partir de la compilation C# 2.0 ou de C# 3.0 qui intègre les méthodes d'extension. Vous pourrez constater que l'IL est complètement identique si on ne prend pas en compte les attributs. En effet, le désassemblage montre clairement que les méthodes d'extension sont « marquées » par des attributs.

.custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()

Une des conséquences est, que si vous observez le code C# via Reflector, l'utilitaire aura fait déjà une reconnaissance de l'attribut « ExtensionAttribute » et aura placé « this » devant le premier paramètre de chaque méthode statique de la classe « ComplementEnumerable » pour son interprétation du code C#.
Ce qu'il faut retenir à ce stade de l'exposé, c'est que les nouveautés C# 3.0 présentées jusqu'à présent, apportent une simplification de la syntaxe dans les requêtes et que celles-ci bénéficient de la même performance puisque « sous le capot », l'IL (Intermediate Language) est identique.
Concernant les méthodes d'extension, la première question que l'on peut se poser est la suivante : Si on « surcharge » une méthode par extension, quel est le comportement du compilateur ?

public static  string ToString(this int entier)

{

    return new StringBuilder("Nombre").Append(entier).ToString();

}

Avec cette extension possible, quelle va donc être la chaîne retournée par la console ?

int i = 5;

Console.WriteLine(i.ToString());

Le résultat montre heureusement que les extensions de méthodes ne cassent pas la logique « objet » au niveau du langage C#.
Les plus déterminés d'entre vous se poseront la question « N'est-ce pas parce que la classe « étendue » est marquée en « sealed » ? ». Vous pouvez répéter la même opération sur une classe non fermée. Le résultat sera heureusement le même.

Un autre problème que l'on peut se poser, c'est la coexistence de deux méthodes d'extension de même définition.

public static class Complement

{

    public static string ToString(this NoteENT maNote, bool isDate)

    {

        if (isDate)

            return maNote.Date.ToShortDateString();

        else

            return maNote.Note.ToString();

    }

}

 

public static class ComplementBis

{

    public static string ToString(this NoteENT maNote, bool isDate)

    {

        if (isDate)

            return maNote.Date.ToShortDateString();

        else

            return maNote.Note.ToString();

    }

}

...

NoteENT t = new NoteENT(){Note=5,Date=new DateTime(2007,10,1)};

Console.WriteLine(t.ToString(true));

Que les extensions soient définies dans la même assembly ou bien dans deux assemblies externes, le compilateur rend son verdict :

The call is ambiguous between the following methods or properties:

    'ConsoleApplication.Complement.ToString(ConsoleApplication.NoteENT, bool)' 

        and 'ConsoleApplication.ComplementBis.ToString(ConsoleApplication.NoteENT, bool)'

en ce qui concerne la même assembly.

The call is ambiguous between the following methods or properties:

    'Library1.Complement.ToString(ConsoleApplication.NoteENT, bool)' 

        and 'Library2.ComplementBis.ToString(ConsoleApplication.NoteENT, bool)'

Lorsque l'on fait appel à des méthodes d'extension de même définition dans deux assemblies différentes.

Il existe une situation que je n'ai pas exposée jusqu'à présent : Celle de la méthode d'extension définie dans une assembly externe, et d'une « surcharge » (terme que l'on peut trouver très abusif) de cette méthode d'extension définie dans l'assembly d'appel. Le compilateur semble donner la priorité à la méthode d'extension « surchargeante ». Mais la conclusion est hâtive. En effet, il suffit que l'espace de nom soit différent entre la méthode d'extension surchargeante et l'appel pour avoir une erreur de compilation ! Donc pour pouvoir effectuer une « surcharge » d'une méthode d'extension existante, il faut que la définition soit faite dans la même assembly et le même namespace que l'appel !

Après ce constat très intéressant, il nous faut évaluer les éventuelles problématiques d'une telle possibilité. Imaginez que vous développiez une assembly où vous avez dû effectuer pour une raison X par exemple une surcharge d'une des méthodes d'extension de Linq située dans le namespace « System.Linq » de l'assembly « System.Core.dll ». Vous serez ravi de la mise à disposition de vos énumérations traitées par la « surcharge ». Par contre l'utilisateur de votre assembly sera très ennuyé quand il utilisera le namespace de votre assembly contenant à la fois les méthodes d'accès aux énumérations qu'ils souhaitent mais aussi votre « surcharge » de méthodes d'extension. En effet, vous comprenez très bien que le compilateur va lui indiquer un « appel ambigu » s'il fait aussi référence à « System.Linq ». Ce cas de figure rend le namespace de votre assembly inutilisable.
Il existe une solution à cette problématique, il suffit de déclarer vos « surcharges » de méthodes d'extension non pas en « public » mais en « internal » limitant ainsi l'accessibilité de ces dernières à votre propre assembly. Par contre, concernant les autres namespaces de votre assembly, c'est à vous d'être vigilant.

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