Introduction aux Expressions Lambda


Pour comprendre l'intérêt des expressions lambda, il est d'abord intéressant de revenir sur plusieurs concepts introduits en C# depuis la version 1.0 :
  • Les délégués
  • Les méthodes anonymes
Nous allons détailler nos explications en nous basant sur un exemple concret.



L'objectif sera d'écrire une fonction qui prendra en paramètre une liste de personnes et de la filtrer sur un critère donné (âge, prénom ...) pour en afficher la liste résultante.

    Notre classe Personne :

    // une classe Personne avec 3 propriétés et accesseurs respectifs.
    class Personne
    {
        // Accesseurs : getters & setters en C# 3.0
        public int Age { get; set; }
        public string Prenom { get; set; }
        public string Nom { get; set; }

        // Une méthode affichant les infos sur une liste de personnes
        public static void displayList(List<Personne> list)
        {
            foreach(Personne p in list)
                Console.WriteLine("Nom : "+p.Nom+", Prenom : "+p.Prenom+", Age : "+p.Age);
        }
    }

    On souhaite obtenir la liste des adultes, notre critère de test sera donc l'âge ( >= 18 ans).
    Ci-dessous la définition de notre fonction displayAdultPersonne(List<personne> listPersonnes) :

    class Personne
    {
        // ...

        // Notre méthode de filtre et d'affichage
        public static void displayAdultPersonne(List<Personne> listPersonnes)
        {
            // contiendra la liste des adultes
            List<Personne> adults = new List<Personne>();

            // test sur l'âge
            foreach (Personne p in listPersonnes)
                if (p.Age >= 18) adults.Add(p);

            Personne.displayList(adults);
        }
    }

    Notre programme de test sur une liste quelconque :

    // fonction main(), point d'entrée pour nos tests.
    static void Main(string[] args)
    {
        // initialisation d'une liste quelconque de Personnes
        List<Personne> personneList = new List<Personne> {
            new Personne { Age = 10, Nom = "Dupond", Prenom = "François"},
            new Personne { Age = 21, Nom = "Dupont", Prenom = "Jean"},
            new Personne { Age = 29, Nom = "Martin", Prenom = "Sarah"},
            new Personne { Age = 17, Nom = "Petit", Prenom = "Nicolas"},
            new Personne { Age = 24, Nom = "Dubois", Prenom = "Marie"}
        };

        // Appel de la fonction displayAdultPersonne() ..
        Personne.displayAdultPersonne(personneList);

        Console.WriteLine("Appuyer Entrée pour quitter ...");
        Console.Read();
    }

    Les résultats à l'exécution :


    La première version de programme est disponible ici.

    Question :

    Maintenant, que se passe-t-il si l'on souhaite filtrer la liste de personnes sur un autre critère ?
    Par exemple, filtrer et afficher les personnes au prenom de 5 lettres ? Créer une autre méthode avec un test sur le nom ?!
    Pas très flexible comme solution ...

    Utilisation des délégués

    C'est là qu'intervient l'utilité des délégués introduits en C# 1.0.
    L'idée sera de passer un paramètre supplémentaire à notre fonction displayAdultPersonne(List<Personne> listPersonnes) : un délégué pointant sur différentes méthodes possibles. Celles-ci auront pour rôle de filtrer : sur l'âge, sur le nom ...etc des personnes.

    Modifions la classe Personne en conséquence :


    Explications :
    • 1. Création du type délégué de type myDelegate.
      Dans notre cas, le délégué pointera sur une méthode qui prendra en argument un type Personne et renverra un booléen, égalant vrai si la condition de filtre est vérifiée (la personne est bien un adulte, ou bien la personne a un prénom de 5 lettres.)
    • 2. Ajout d'un paramètre supplémentaire de type myDelegate à notre fonction displayAdultPersonne(List<Personne> listPersonnes)
    • 3. Création d'une méthode de filtre sur l'âge
    • 4. Création d'une méthode de filtre sur les prénoms contenant 5 lettres
    Pour appeller la fonction displayAdultPersonne(List<Personne> listPersonnes, myDelegate filter), on initialise notre délégué de type myDelegate avec nos méthodes de filtre isAdult(Personne p) ou bien matchNameLength(Personne p).

    Console.WriteLine("Filtre sur les personnes adultes :\n");           

    // Instanciation du délégué
    Personne.myDelegate filterAge = new Personne.myDelegate(Personne.isAdult);

    // Appel de la fonction displayAdultPersonne() avec le délégué en paramètre
    Personne.displayAdultPersonne(personneList, filterAge);

    ou bien :

    Console.WriteLine("\nFiltre sur les personnes à prenom avec 5 lettres :\n");

    // Instanciation du délégué
    Personne.myDelegate filterPrenom = new Personne.myDelegate(Personne.matchNameLength);

    // Appel de la fonction displayAdultPersonne() avec le délégué en paramètre
    Personne.displayAdultPersonne(personneList, filterPrenom);

    A l'exécution du programme, nous obtenons les résultats :


    Désormais, la fonction displayAdultPersonne n'effectue plus le rôle de filtre puisque nous l'avons paramétrée pour transmettre ce rôle à un délégué.
    Ok, nous avons rendu flexible le code. Et ce, au prix d'une complexité non négligeable.

    Nous allons simplifier tout cela pour arriver à notre objectif qui reste le même : filtrer une liste de personnes sur un critère donné.

    La version évoluée de programme est disponible ici.

    Utilisation du délégué Predicate<T>

    Au lieu de créer notre propre type délégué, nous pouvons utiliser le type Predicate<T> existant et introduit en C# 2.0 :
    delegate booléen Predicate<T>(T obj);

    Le type Predicate<T> est un délégué générique.
    Il représente une méthode qui retourne un booléen en fonction de son argument générique T en entrée.
    Notre type T ici sera le type Personne.

    La fonction displayAdultPersonne de la classe Personne devient alors :

    class Personne
    {
        // ...

        // Notre méthode d'affichage avec le délégué Predicate<T> en paramètre.
        public static void displayAdultPersonne(List<Personne> listPersonnes,
                                                    Predicate<Personne> filter)
        {
            // contiendra la liste des adultes
            List<Personne> adults = new List<Personne>();

            foreach (Personne p in listPersonnes)
                if (filter(p))
                    adults.Add(p);

            Personne.displayList(adults);
        } 

        // Méthode de filtre sur l'âge
        public static bool isAdult(Personne p)
        {
            if(p.Age >= 18)
                return true;
            else return false;
        }

        // Méthode de filtre sur les prénoms de 5 lettres
        public static bool matchNameLength(Personne p)
        {
            if (p.Prenom.Length == 5)
                return true;
            else return false;
        }

    }

    Commentaires :
    • Plus besoin de créer un type délégué spécifique (myDelegate)
    • Plus besoin d'initialiser ce délégué avec les méthodes de filtre isAdult(Personne p) ou matchNameLength(Personne p)
    • On appelle simplement notre fonction displayAdultPersonne directement avec une des méthodes de filtre en paramètre

    // Appel de la fonction displayAdultPersonne() avec le type Predicat<T> en arg
    Console.WriteLine("Filtre sur les personnes adultes :\n");
    Personne.displayAdultPersonne(personneList, Personne.isAdult);

    Console.WriteLine("\nFiltre sur les personnes à prenom avec 5 lettres :\n");
    Personne.displayAdultPersonne(personneList, Personne.matchNameLength);

    Nous obtenons les mêmes résultats que précédemment (avec l'utilisation du délégué myDelegate en moins).


    La version évoluée du programme est disponible ici.

    Utilisation des méthodes anonymes

    Pour simplifier l'utilisation des délégués, les méthodes anonymes sont apparues en C# 2.0.
    Leur intérêt est de permettre la déclaration inline de la méthode pointée par le délégué, donc sans avoir à la nommer.

    Par conséquent, il n'est plus utile de définir dans la classe Personne, les méthodes nommées isAdult(Personne p) ou matchNameLength(Personne p).

    // Appel de la fonction displayAdultPersonne avec les méthodes anonymes
    Console.WriteLine("Filtre sur les personnes adultes :\n");
    Personne.displayAdultPersonne(personneList, delegate(Personne p) { return p.Age >= 18; });

    Console.WriteLine("\nFiltre sur les personnes à prenom avec 5 lettres :\n");
    Personne.displayAdultPersonne(personneList, delegate(Personne p) { return p.Prenom.Length == 5; });

    L'appel se fait sur une seule ligne, et le code est beaucoup plus lisible pour obtenir à l'exécution les mêmes résultats que précédemment.

    La version évoluée du programme est disponible ici.

    Les lambda expressions

    Enfin, nous arrivons aux lambda expressions qui permettent de simplifier tout ce que nous avons vu jusqu'à présent.
    Pour filtrer sur notre liste de personnes, au lieu d'utiliser des délégués ou des méthodes anonymes, on peut utiliser une expression lambda équivalente.

    // Appel de la fonction displayAdultPersonne avec les lambda expressions !

    Personne.displayAdultPersonne(personneList, p => p.Age >= 18);
    Personne.displayAdultPersonne(personneList, p => p.Prenom.Length == 5);

    Ci-dessus, notre expression lambda p => p.Age >=18

    se traduit comme :

    "Pour toute Personne p, on renvoie vrai si l'âge est supérieur à 18."

    Ainsi, les expressions lambda permettent de parvenir à notre objectif par une approche déclarative plutôt que programmatique (i.e exprimer ce que l'on souhaite obternir par une série d'instructions en if, boucle for ..).

    Les expressions lambda sont des fonctions sans nom qui peuvent être utilisées partout où le type délégué est valide.

    Une expression lambda est constituée de 3 parties :
    • un ou plusieurs paramètres d'entrée
    • l'opérateur =>
    • une expression à évaluer ou une série d'instructions
    Dans notre exemple, puisque notre fonction displayAdultPersonne prend en paramètre un délégué de type Personne et renvoie un booléen :
    il est indispensable que l'expression lambda équivalent prenne en argument un type Personne et renvoie un booléen.

    Un peu plus compliqué, pour récupérer la liste des adultes dont le prénoms contiennent 5 lettres :

    Personne.displayAdultPersonne(personneList, p => p.Age >=18 && p.Prenom.Length == 5);

    La version évoluée de programme est disponible ici.

    A première vu, comprendre les expressions lambda n'est pas une chose simple.
    Derrière ce nom bizzaroïde, se cache toute la puissance de la programmation fonctionnelle.

    0 commentaires:

    Enregistrer un commentaire