C# – Méthodes d'extension

En POO, il est d'usage de limiter les méthodes d'instance à un ensemble minimal ayant besoin de privilèges d'accès particuliers aux membres privés d'une classe, et d'essayer pour le reste d'enrichir l'interface d'une classe de services auxiliaires non-intrusifs. Exprimé autrement, si une fonction peut être exprimée efficacement en termes des membres publics d'une classe, il est préférable de réduire le couplage et de l'exprimer autrement que par une méthode d'instance de cette classe.

Cela dit, les outils (les IDE, par exemple) sont conçus de manière à faciliter la programmation lorsqu'un service est exposé sous forme d'un membre d'instance. Pensez aux diverses fonctions d'autocomplétion de vos outils, qui suggèrent diverses options lorsque vous suivez un nom de classe ou d'instance d'un point (.). Cette convivialité pour les programmeuses et les programmeurs tend à aller à l'encontre de ce que les années de pratique nous ont enseigné.

Il se trouve que C# offre un mécanisme pour pallier cet irritant, et faciliter la représentation de services externes à une classe comme faisant conceptuellement partie de cette classe. Ce mécanisme est nommé « méthode d'extension ».

En détail

Supposons une classe Point simpliste :

public class Point
{
   public int X { get; } = 0;
   public int Y { get; } = 0;
   public Point()
   {
   }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}

Sur la base de cette classe Point, construisons une classe Cercle elle aussi simpliste :

public class Cercle
{
   // par défaut, cercle unitaire
   public Point Centre { get; private set; } = new Point();
   public float Rayon { get; private set; } = 1.0f;
   public Cercle()
   {
   }
   public Cercle(Point centre, float rayon)
   {
      Centre = centre;
      Rayon = rayon;
   }
}

Supposons que nous souhaitions exposer un service de calcul de la distance entre deux instances de Cercle, et que ce mécanisme passe par le calcul de la distance entre leurs centres (donc la distance entre deux instances de Point). Il se trouve que ce calcul peut être exprimé simplement à partir de l'interface publique de ces deux classes. Par exemple :

using statit System.Math;
public static class AlgosGéométrie
{
   public static float Distance(Point p0, Point p1) =>
      (float) Sqrt(Pow(p0.X - p1.X, 2) + Pow(p0.Y - p1.Y, 2));
   public static float Distance(Cercle c0, Cercle c1) =>
      Distance(c0.Centre, c1.Centre);
}

Tout cela fonctionne bien, mais demandera à l'usage d'exprimer le calcul de la distance entre deux instances de Cercle nommées c0 et c1 sous la forme Distance(c0,c1). Si vous estimez qu'il serait pertinent de permettre d'écrire aussi c0.Distance(c1), alors les méthodes d'extension vous sembleront intéressantes.

Les règles

 Une méthode d'extension est une méthode de classe (static) accessible et placée dans une classe à la fois qualifiée public et static; c'est d'ailleurs le cas des méthodes Distance(Point,Point) et Distance(Cercle,Cercle) de la classe AlgosGéométrie ci-dessus. Il faut que ces méthodes acceptent au moins un paramètre, pour des raisons qui seront rapidement évidentes.

Pour être une méthode d'extension, le premier paramètre de la méthode doit se voir apposer le mot clé this. Cela signifie que, dans le cas où le code client le souhaite, ce paramètre pourra être utilisé syntaxiquement comme l'instance propriétaire de la méthode, même s'il n'en est rien.

Ainsi, notre classe AlgosGéométrie deviendrait :

using statit System.Math;
public static class AlgosGéométrie
{
   public static float Distance(this Point p0, Point p1) =>   // notez le this sur p0
      (float) Sqrt(Pow(p0.X - p1.X, 2) + Pow(p0.Y - p1.Y, 2));
   public static float Distance(this Cercle c0, Cercle c1) => // notez le this sur c0
      Distance(c0.Centre, c1.Centre); // ou c0.Centre.Distance(c1.Centre);, au choix
}

Enrichir l'interface d'une collection

Supposons un programme simple vérifiant que tous les éléments d'une List<int> soient pairs :

// ...
static bool SontTousPairs(List<int> lst)
{
   foreach (int n in lst)
      if (n % 2 != 0)
         return false;
   return true;
}
static void Main()
{
   var lst = new List<int>() { 2, 3, 4, 6, 8, 10 };
   if(SontTousPairs(lst))
   {
      Console.WriteLine("Ils sont tous pairs...? (suspect)");
   }
   else
   {
      Console.WriteLine("Il semble y avoir au moins un intrus");
   }
}

L'algorithme SontTousPairs est très spécifique; écrire de telles fonctions est contre-productif, car on se trouve à réécrire souvent le même algorithme. Supposons maintenant que nous ayons conçu un algorithme générique applicable à une List<T>, capable de remplacer efficacement cet algorithme très spécifique :

// ...
static bool RespectentTous<T>(List<T> lst, Func<T,bool> pred)
{
   foreach (T obj in lst)
      if (!pred(obj))
         return false;
   return true;
}
static void Main()
{
   var lst = new List<int>() { 2, 3, 4, 6, 8, 10 };
   if(RespectentTous(lst, n => n % 2 == 0))
   {
      Console.WriteLine("Ils sont tous pairs...? (suspect)");
   }
   else
   {
      Console.WriteLine("Il semble y avoir au moins un intrus");
   }
}

Les algorithmes de ce type peuvent s'exprimer aisément sous forme de méthode d'extension :

// ...
public static class Algos
{
   // deux exemples parmi des tas de possibilités
   public static bool RespectentTous<T>(this List<T> lst, Func<T,bool> pred)
   {
      foreach (T obj in lst)
         if (!pred(obj))
            return false;
      return true;
   }
   public static bool CompterSi<T>(this List<T> lst, Func<T,bool> pred)
   {
      int n = 0;
      foreach (T obj in lst)
         if (pred(obj))
            ++n;
      return n;
   }
}
// ...
static void Main()
{
   var lst = new List<int>() { 2, 3, 4, 6, 8, 10 };
   if(lst.RespectentTous(n => n % 2 == 0)) // voilà!
   {
      Console.WriteLine("Ils sont tous pairs...? (suspect)");
   }
   else
   {
      Console.WriteLine("Il semble y avoir au moins un intrus");
   }
   Console.WriteLine($"Il y a {lst.Count} éléments, dont {lst.CompterSi(n => n % 2 == 0)} sont pairs");
}

Généraliser

Il se trouve que List<T> implémente IEnumerable<T>, ce qui permet d'utiliser foreach sur cette collection, mais la majorité des collections de .NET implémentent aussi cette interface. Si nous exprimons nos algorithmes génériques sur la base de cette interface, conséquemment, nous pouvons enrichir même l'interface des humbles tableaux :

// ...
public static class Algos
{
   // deux exemples parmi des tas de possibilités
   public static bool RespectentTous<T>(this IEnumerable<T> lst, Func<T,bool> pred) // ICI
   {
      foreach (T obj in lst)
         if (!pred(obj))
            return false;
      return true;
   }
   public static bool CompterSi<T>(this IEnumerable<T> lst, Func<T,bool> pred) // ICI
   {
      int n = 0;
      foreach (T obj in lst)
         if (pred(obj))
            ++n;
      return n;
   }
}
// ...
static void Main()
{
   var lst = new List<int>() { 2, 3, 4, 6, 8, 10 };
   if(lst.RespectentTous(n => n % 2 == 0)) // voilà!
   {
      Console.WriteLine("Ils sont tous pairs...? (suspect)");
   }
   else
   {
      Console.WriteLine("Il semble y avoir au moins un intrus");
   }
   Console.WriteLine($"Il y a {lst.Count} éléments, dont {lst.CompterSi(n => n % 2 == 0)} sont pairs");
   Console.WriteLine(new int[]
   {
      2, 3, 5, 7, 11
   }.CompterSi(n => n % 2 != 0)); // ICI
}

Ne pas réinventer la roue – LiNQ

Notez que j'ai un vieil article sur cette technologie que vous trouverez sur Linq.html si vous le souhaitez, mais notez aussi que cet article montre son âge.

Avec C# est livrée une bibliothèque nommée LiNQ (Language-Integrated Queries). Cette bibliothèque permet entre autres de réaliser des opérations rappelant celles du langage SQL à même le code C#, mais offre plus spécifiquement toute une gamme d'algorithmes exprimés sous forme de méthodes d'extension. Vous pouvez d'ailleurs l'observer si vous examinez, avec votre IDE de prédilection, l'ensemble restreint de fonctions exposées à travers List<T> avant et après avoir inséré la ligne using System.Linq; dans un projet : les deux listes de fonctionnalités ne seront pas du tout de la même envergure.

Vous trouverez une liste exhautsive de ces algorithmes sur https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable?view=netframework-4.8..

Par exemple, le programme avec fonctionnalités génériques exprimées sous forme de méthodes d'extension comme RespectentTous() et CompterSi(), plus haut, peut s'exprimer plus simplement comme suit :

using System;
using System.Linq;
namespace z
{
   class Program
   {
      static void Main()
      {
         var lst = new List<int>() { 2, 3, 4, 6, 8, 10 };
         if(lst.All(n => n % 2 == 0)) // ICI
         {
            Console.WriteLine("Ils sont tous pairs...? (suspect)");
         }
         else
         {
            Console.WriteLine("Il semble y avoir au moins un intrus");
         }
         Console.WriteLine($"Il y a {lst.Count} éléments, dont {lst.Count(n => n % 2 == 0)} sont pairs"); // ICI
         Console.WriteLine(new int[]
         {
            2, 3, 5, 7, 11
         }.Count(n => n % 2 != 0)); // ICI
      }
   }
}

Voilà.

Lectures complémentaires

Quelques liens pour enrichir le propos.

(à venir)


Valid XHTML 1.0 Transitional

CSS Valide !