Frédéric Mélantois
Surcharger une méthode d'extension
La nouvelle version de C# 3.0 apporte la possibilité d'« étendre » les classes via les méthodes d'extension. Quel est le comportement par défaut du compilateur dans le cas de définitions identiques de deux méthodes d'extension ?
Par Frédéric Mélantois publié le 25/11/2007 à 21:40, lu 14990 fois,
L'article a nécessité une correction post-publication le 2 décembre. Il m'a paru important de conserver le contenu initial de ce dernier par transparence vis-à-vis des premiers lecteurs mais aussi parce que l'erreur commise a un intérêt pédagogique. Un chapitre « Mise à jour » a donc été ajouté en fin d'article, le reste n'ayant pas été modifié.
Pour définir une méthode d'extension, il suffit de créer une classe statique. Ensuite, le premier paramètre de chaque méthode sera « balisé » par le mot clé « this » afin de désigner le type de l'objet que l'on souhaite « étendre ».
Prenons un exemple. Soit un objet « Personne » ayant pour propriétés : un prénom, un nom et un âge.

public class Personne

{

    public string Nom { get; set; }

    public string Prenom { get; set; }

    public int Age { get; set; }

}

Si vos objets sont alimentés par différentes sources, vous vous apercevrez que le nom est parfois en majuscule, parfois en minuscule, ce qui vous posera problème par exemple pour écrire de façon standard un courrier sous Word.

List<Personne> personnes = new List<Personne>();

personnes.Add(new Personne(){Nom="DURAND",Prenom="Cyril",Age=21});

personnes.Add(new Personne() { Nom = "Kempé", Prenom = "Laurent", Age = 36 });

personnes.Add(new Personne() { Nom = "MELANTOIS", Prenom = "Frédéric", Age = 38 });

personnes.Add(new Personne() { Nom = "Perfetti", Prenom = "Michel", Age = 30 });

Pour nos documents, nous aimerions bien avoir une méthode « PrenomNom ()» dans la classe « Personne », permettant d'avoir le prénom puis le nom en majuscules. Mais dans bien des cas, vous n'aurez pas la possibilité de réécrire cette classe. Les méthodes d'extension viennent alors à notre secours :

namespace EspaceDeNom1

{

    public static class ExtensionPersonne

    {

        //prénom + Nom en majuscule

        //[//System.Runtime.CompilerServices.Extension]

        public static string PrenomNom(this Personne p)

        {

            StringBuilder s = new StringBuilder(p.Prenom);

            s.Append(" ");

            s.Append(p.Nom.ToUpper());

            return s.ToString();

        }

    }

}

Veuillez noter que le compilateur C# refuse l'utilisation de l'attribut « System.Runtime.CompilerServices.ExtensionAttribute » contrairement à Visual Basic. La seule façon de créer une méthode d'extension est d'ajouter le mot clé « this ». Sous le capot, comme nous avons pu le voir dans un précédent article, le « this » est retranscrit en attribut sur la classe et la méthode statique au niveau de l'IL (Intermediate Language).
Le compilateur permet un usage aisé de la méthode :

foreach (string identite in from p in personnes select p.PrenomNom())

{

    Console.WriteLine(identite);

}

Ce qui nous donne le résultat suivant :
 
/content/c4a367d4-f82c-47e0-812a-1f35ce4b86f8/resultat2.jpg
 
Nous pouvons constater qu'un des noms comporte une majuscule avec accent, ce qui ne nous convient pas. Imaginons que nous ne sommes pas en possession des sources de la méthode d'extension ou que celle-ci se trouve dans une assembly déjà déployée. Nous avons très largement employé la méthode d'extension « PrenomNom() » un peu partout dans notre projet. Celle-ci satisfait nombre de vos collègues mais pas vous dans votre propre utilisation. Vous pouvez alors soit modifier son utilisation par un remplacement systématique par une autre méthode soit « surcharger » la méthode d'extension. Cette dernière solution est bien évidemment bien moins lourde !
La surcharge d'une méthode d'extension est possible sous certaines conditions. Celle-ci doit se trouver dans la même assembly et le même espace de nom que l'appel à la méthode d'extension. Si ces conditions ne sont pas remplies, le compilateur renverra une erreur signifiant un appel ambigu de méthodes d'extension. Ecrivons rapidement une méthode d'extension (la performance extrême dans les exemples n'est pas l'objet de l'article)

public static class ExtensionPersonne

{

    internal static string UpperToStandardUpper(this string chaine)

    {

        string accent =    "ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜ";

        string sansAccent = "AAAAAACEEEEIIIINOOOOOOUUUU";

        char[] tableauSansAccent = sansAccent.ToCharArray();

        char[] tableauAccent = accent.ToCharArray();

        for (int i = 0; i < accent.Length; i++)

        {

            chaine = chaine.Replace(tableauAccent[i].ToString(), tableauSansAccent[i].ToString());

        }

        return chaine;

    }

 

    public static string PrenomNom(this Personne p)

    {

        StringBuilder s = new StringBuilder(p.Prenom);

        s.Append(" ");

        s.Append(p.Nom.ToUpper().UpperToStandardUpper());

        return s.ToString();

    }

}

Si nous ajoutons ce code dans le même espace de nom que l'appel, une surcharge de méthode d'extension s'opère. De sorte que la requête suivante :

foreach (string identite in from p in personnes select p.PrenomNom())

{

    Console.WriteLine(identite);

}

renvoie :
 
/content/c4a367d4-f82c-47e0-812a-1f35ce4b86f8/Resultat3.JPG
 
Nous venons de réaliser avec succès une surcharge de méthode d'extension. Cet aspect nous apparaît bien pratique. Toutefois, sachez que bien évidemment vous pouvez passer outre par l'invocation direct des méthodes :

foreach (string identite in from p in personnes select EspaceDeNom1.ExtensionPersonne.PrenomNom(p))

    ou

foreach (string identite in from p in personnes select ConsoleApplication1.ExtensionPersonne.PrenomNom(p))

Les méthodes d'extension sont très séduisantes mais peuvent générer des conflits. En effet, il suffit d'utiliser deux espaces de noms différents comportant une signature de méthode d'extension identique. C'est alors que le compilateur génère une erreur d'appel ambigu de méthodes d'extension. Comment éviter au maximum ces conflits ? Comme le suggère la documentation MSDN, il faut rassembler ses méthodes d'extension sous un même espace de nom bien évocateur : « Extensions ». Si ceci est une bonne pratique, car elle permet de localiser très rapidement l'ensemble de vos méthodes d'extension, cela n'évitera pas les conflits si un éditeur de logiciel a créé une même signature que vous. Une solution peut être de préfixer chacun de vos noms de méthode d'extension. C'est à vous de bien penser au nom que vous allez donner à votre méthode. Il existe une solution pour limiter l'impact de vos méthodes d'extension, c'est de changer la portée de « public » à « internal ». Nous l'avons vu dans un précédent exemple avec la méthode « UpperToStandardUpper ». Sa portée est donc limitée à l'assembly de sorte que les utilisateurs de cette dernière n'ont pas accès à cette méthode et n'auront donc pas de conflit. C'est une pratique à généraliser dès les premières heures d'utilisation de c# 3.0 ou de Visual Basic 9
Dans certains cas, il peut être intéressant de surcharger une méthode d'extension. Si vous lisez cet article, vous avez sans doute déjà utilisé Linq et en particulier Linq to SQL. Pour ceux d'entre vous qui l'utiliseront, se posera une problématique concernant la couche d'accès aux données que vous aurez construite. En effet, si nous proposons une méthode renvoyant un IEnumerable<T> dans notre DAL

public IEnumerable<PERSONNE> GetPersonnes()

{

    DataClasses1DataContext t = new DataClasses1DataContext(_myDatabaseConnectionString);

    var c = from h in t.PERSONNEs select h;

    return c;

}

Vous ne pourrez empêcher personne d'écrire dans la couche de présentation ceci :

IQueryable<PERSONNE> q = (IQueryable<PERSONNE>) MyDAL.GetPersonnes();

var r = from s in q where s.Personne_Age > 30 select s;

ce qui aura pour incidence de créer un nouvel accès à la base de données par l'usage du IQueryable. Pour éviter, cette problématique, il faut que notre couche d'accès ne renvoie que des IEnumerable aucunement IQueryable. Nous pouvons réitérer de manière à ne plus avoir d'IQueryable de la façon suivante :

public IEnumerable<PERSONNE> GetPersonnes()

{

    DataClasses1DataContext t = new DataClasses1DataContext(_myDatabaseConnectionString);

    var c = from h in t.PERSONNEs select h;

    return from i in c.AsEnumerable() select i;

}

Mais si nous devons appliquer ces changements à chaque méthode de notre couche d'accès, ce travail devient fastidieux et nous risquons d'en oublier. La surcharge de méthodes d'extension peut venir à notre secours dans ce cas, en surchargeant la méthode d'extension « Select » de Linq To Objects. Voici le code de la surcharge :

internal static class ExtensionLinq

{

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

   Func<TSource, TResult> selector)

    {

        IEnumerable<TSource> sourceEnum = source;

        foreach (TSource s in sourceEnum)

            yield return selector.Invoke(s);

    }

}

Cette surcharge permet de ne plus avoir à ce soucier dans la couche de présentation des manipulations telles que :

IQueryable<PERSONNE> q = (IQueryable<PERSONNE>) MyDAL.GetPersonnes();

var r = from s in q where s.Personne_Age > 30 select s;

Elles deviennent impossibles. De sorte, que les requêtes Linq en couche de présentation ne se font qu'en mémoire. Vous aurez remarqué que j'ai pris soin d'écrire ma surcharge avec la portée « internal » de sorte qu'il n'y ait pas d'erreur d'appel ambigu de méthodes d'extension dans ma couche de présentation en faisant usage de Linq.
Je tiens vivement à remercier Flavien pour m'avoir notifié une ligne de code inutile. En ont découlé des discussions très instructives avec Flavien CHARLON et Matthieu MEZIL qui m'ont amené à retester différents cas de figure et à montrer que la surcharge du « Select » pour « Linq To SQL » n'était pas une solution satisfaisante.
Rappelons le code proposé :

internal static class ExtensionLinq

{

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

   Func<TSource, TResult> selector)

    {

        IEnumerable<TSource> sourceEnum = source;

        foreach (TSource s in sourceEnum)

            yield return selector.Invoke(s);

    }

}

la ligne « IEnumerable<TSource> sourceEnum = source; » est complètement inutile car le « yield return » fabrique sous le capot un objet privé implémentant IEnumerable<T> et non IQueryable<T>. Voici le code que j'aurai dû écrire :

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

Func<TSource, TResult> selector)

 {

    foreach (TSource s in source)

        yield return selector.Invoke(s);

 }

On pourrait être satisfait mais le Select ne fonctionne pas comme on le souhaiterait. En effet, si au lieu de renvoyer toutes les colonnes de la table Personne, nous sélectionnons uniquement le nom par exemple :

var c = from h in dt.PERSONNEs where h.Personne_Age > 30 select h.Personne_Nom;

La requête effectuée vers la base de donnée renverra toutes les colonnes avant que n'opère le « Select » que nous avons écrit. Cette solution n'est donc pas satisfaisante. Si l'on souhaite que la base de données ne renvoie que les colonnes que l'on a réellement besoin, il faut faire appel à la méthode d'extension « Select » de « System.Linq.Queryable » :

internal static IEnumerable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source,

    System.Linq.Expressions.Expression<Func<TSource, TResult>> selector)

{

    IQueryable iq = Queryable.Select(source, selector);

    foreach (TResult s in iq)

        yield return s;

}

Si vous utilisez le Debugger ou le système de log du « DataContext », vous pourrez constater que cette surcharge répond désormais à notre exigence.
Toutefois, les discutions avec mes camarades m'ont amené à effectuer de nombreux tests. J'ai alors pu constater que cette surcharge ne fonctionnait pas dans certains cas. Par exemple, si vous écrivez une requête du style « select * from matable where condition », le compilateur ne génèrera que « Table<matable>.Where(condition) » car le « Select » est inutile car toutes les colonnes sont demandées. On peut donc se féliciter d'un tel comportement pour ce qui est de la performance. Mais, la surcharge que je vous propose, ne fonctionne donc pas dans tous les cas. Je vous recommande donc de ne pas l'employer dans votre couche d'accès. Vous pouvez écrire une méthode d'extension qui empêchera de pouvoir requêter de nouveau via « Linq to SQL » :

internal static IEnumerable<TSource> ToLinqToObject<TSource>(this IQueryable<TSource> source)

{

    foreach (TSource s in source)

        yield return s;

}

Son utilisation se fera comme suit :

public IEnumerable<string> GetPersonnes()

{

    DataClasses1DataContext t = new DataClasses1DataContext(myDatabaseConnectionString);

    var c = from h in t.PERSONNEs where h.Personne_Age > 30 select h;

    return c.ToLinqToObject();//return from i in c.AsEnumerable() select i;

}

Voici les liens des différentes discussions avec Flavien et Matthieu :
Nous venons de montrer que les surcharges de méthodes d'extension pouvaient s'avérer très utiles mais qu'elles doivent être manipuler avec précautions. Voici quelques liens qui peuvent vous être utiles :
 
» Démarrer une discussion
 
 
Discussion démarée par Frédéric Mélantois le 27/11/2007 à 13:16, 1 commentaire(s).
Discussion démarée par Frédéric Mélantois le 17/12/2007 à 17:41, 1 commentaire(s).