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, lu 6925 fois, 6 pages
 4 | Les méthodes d'extension
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.
 
» Démarrer une discussion
 
Discussion démarée par Matthieu Mezil le 22/10/2007 à 11:37, 1 commentaire(s).