Les lambda expression et delegate

Explication des lambda expression, Action et Func

03/07/2011 1546 lectures 4 commentaires 4.43/5 (7 votes)
Dans un cours précédent, nous avons vu comment créer des pointeurs de fonctions en C#, autrement appelés « delegate ». Pour rappel, un « delegate » permet de passer une méthode à une autre méthode. Cependant, tel que nous l’avons vu, la syntaxe est un peu lourde, c’est pourquoi le Framework 3.0 propose les « lambda expressions » et le 3.5 les « delegate » de type « Func » et « Action ».

Les lambda expressions


Une « lambda expressions » est une notion nouvelle en 3.0 qui peut dérouter au premier coup d’œil. Effectivement, on entend souvent les gens qui n’ont jamais vu de « lambda expressions » dire « Oui mais, pour quelqu’un qui connait pas ça, c’est totalement illisible ». Je suis d’accord mais je pense que c’est une très mauvaise raison pour ne pas les utiliser. En effet, cette vision des choses est applicable à n’importe quelle notion. Nous vous conseillons de ne pas partir sur cette idée en apprenant les « lambda expression ».

Actuellement, dans le cours sur les delegates, nous avons vu comment créer des pointeurs de méthodes. C’est très pratique mais il faut reconnaitre que c’est très contraignant. Voyez par exemple ce code :



Nous commençons tout d’abord par créer un « delegate » ne renvoyant pas de valeur (« void ») et n’attendant pas de paramètre. Nous déclarons également une première méthode qui attend simplement un « delegate » de ce type en paramètre et qui se contente d’exécuter la méthode qu’il contient. Nous avons ensuite la méthode « DisplayHello » qui se charge d’afficher « Bonjour ». Enfin, dans la méthode « main », nous appelons la méthode « DoWithDelegate » en passant une référence à la méthode « DisplayHello » pour l’exécuter. Si nous exécutons ce code, nous obtenons :

Image


Cela fonctionne bien, mais vous reconnaitrez volontiers que le code pour faire cela est trop conséquent par rapport au résultat. C’est là que les « lambda expressions » entrent en jeu. Au lieu d’expliquer la théorie de long en large avant de passer à l’exemple, nous allons directement voir comment simplifier le code précédent avec une « lambda expression ». Sur base de ça, nous expliquerons la théorie :



Une modification importante dans ce code est la suppression de la méthode « DisplayHello ». Comme cette méthode n’existe plus, le code qui appelle la fonction « DoWithDelegate » est également modifié et utilise une « lambda expression ». Une expression de ce type possède le format suivant :



En fait, une « lambda expression » va encapsuler une méthode anonyme que nous pourrons passer à une autre méthode et récupérer dans un « delegate ». En fait, la partie à gauche du signe « => » permet de faire une référence aux paramètres passés au « delegate ». Dans le cas présent, le « delegate delegateType » n’attend pas de paramètre, nous mettons donc des parenthèses vides. Nous verrons un peu plus tard comment gérer les paramètres.

Ici, nous avons donc l’expression « () => Console.WriteLine(...) ». La partie gauche permet donc de définir les paramètres du « delegate » (ici vide car « delegateType » n’attend pas de paramètre). La partie droite de l’expression, quant à elle, attend le corps de la méthode qui sera encapsulée dans le « delegate ». En l’occurrence, nous voulons simplement afficher « Bonjour » à la console.

Voyons maintenant comment faire si nous voulons afficher « Bonjour » suivi d’un nom entré par l’utilisateur. Le code deviendrait alors le suivant :



L’exécution de ce code produirait le résultat suivant :

Image


La première différence est que « delegateType » attend maintenant un paramètre du type « string ». La deuxième différence est que dans la méthode « DoMethodWithDelegate », nous demandons le nom à l’utilisateur. Ensuite, nous faisons passer ce nom comme paramètre au « delegate ». Cette fois, la « lambda expression » est la suivante :



Comme dit précédemment, la partie gauche de l’expression va permettre de créer des objets temporaires qui stockeront la valeur des paramètres passés au « delegate » (en l’occurrence, la variable « nom » passée au « delegate » dans la méthode « DoMethodWithDelegate »). Ensuite, nous définissons le contenu de la méthode que nous plaçons dans le « delegate ». Nous indiquons que « Bonjour » suivi de la valeur du paramètre « nom » doit être affiché à la console. Ici, comme il n’y a qu’un seul paramètre au « delegate », il est inutile de mettre les parenthèses. Celles-ci ne sont utiles que lorsque qu’il n’y a pas de paramètre ou plus que un paramètre. Voyez par exemple ce code :



Nous avons donc modifié « delegateType » pour indiquer qu’un deuxième paramètre de type « int » est attendu. Dans la méthode « DoMethodWithDelegate », nous demandons l’âge à l’utilisateur et nous faisons passer cet âge au « delegate ». Ensuite, nous modifions la « lambda expression ». Nous mettons donc les paramètres entre parenthèses en incluant un paramètre nommé « age ». Nous incluons ensuite ce paramètre dans la chaîne de caractères affichée.

Evidemment, dans la « lambda expression », il n’est pas obligatoire de nommer les paramètres de la même façon que ceux défini dans le « delegate ». Effectivement, le nom que vous leur donnez dans la « lambda expression » est juste un moyen de faire référence aux paramètres en question. La seule chose qui ait de l’importance est l’ordre des paramètres :

Image


Comme vous pouvez le voir, l’ordre des paramètres va être respecté. Ainsi, le premier paramètre de la « lambda expression » sera une référence au premier paramètre de « delegateType », donc du type « string ». De la même manière, le second paramètre de l’expression fera référence au second paramètre de « delegateType » et sera donc du type « int ».

Une autre précision importante est qu’il est possible d’utiliser plus d’une ligne de code dans la partie droite de l’expression, mais dans ce cas, il faut les placer entre accolades :



Maintenant que nous avons vu comment utiliser les « lambda expressions », nous allons voir comment utiliser les « Action » pour simplifier encode le code précédent.

Action


Quand nous utilisons un « delegate », il est souvent contraignant de devoir définir celui-ci pour ensuite le passer à une méthode grâce à une « lambda expression ». En 3.5, « Action » nous permet de nous passer de l’étape de déclaration du « delegate ».

Voyez donc le code suivant :



Nous commençons par déclarer un « delegate » ne renvoyant pas de valeur (vu que le type de retour est « void »), que l’on nomme « delegateType ». Ce « delegate » n’attend pas de paramètre. En résumé, ce « delegate » servira à contenir une référence à une méthode ne prenant pas de paramètre et ne renvoyant pas de valeur.

Nous déclarons ensuite une méthode qui prend en paramètre un « delegate » de ce type et qui se charge simplement d’invoquer la méthode qu’il contient. Pour appeler cette méthode, ce n’est pas très compliqué, il suffit simplement de faire passer une « lamba expression » contenant la méthode à placer dans le « delegate » et donc à exécuter. Ici, nous faisons passer un « Console.WriteLine » pour afficher du texte. Si nous exécutons ce code, nous obtenons :

Image


Si nous résumons, nous avons créé un « delegate » ne renvoyant pas de valeur et n’attendant pas de paramètre. Nous avons ensuite créé une fonction qui attendant un « delegate » de ce type en paramètre et qui exécutait la méthode qu’il contient. Enfin, nous avons appelé cette méthode en lui faisant passer une instruction simple permettant d’afficher « Bonjour » à l’écran. Ici, il n’y a rien de bien nouveau.

Nous pouvons compliquer un peu la chose en transformant le code de cette manière :



Ici, la différence avec le code précédent est que cette fois, le « delegate » attend un paramètre de type « string ». La méthode « DoMethodWithDelegate » commence donc par demander le nom à passer en paramètre à ce « delegate » avant de l’exécuter. Dans la méthode « Main », nous appelons cette méthode en indiquant que nous allons utiliser le paramètre du « delegate » en l’affichant précédé de « Bonjour ». Si nous exécutons ce code, nous obtenons naturellement :

Image


Effectivement, cela fonctionne bien, mais c’est assez rébarbatif de toujours devoir déclarer un objet « delegate ». Effectivement, selon le nombre et le type des paramètres attendus, nous pouvons nous retrouver avec beaucoup de déclaration de « delegate ». C’est là que les « Action » entrent en... action (ui, je sais, je suis drôle).

Nous allons voir comment simplifier la deuxième version du code en utilisant les « Action ». La nouvelle version du code ressemblerait donc à ceci :



La première chose qui saute aux yeux est bien évidemment le fait que nous n’avons plus déclaré d’objet de type « delegate ». Au lieu de ça, nous définissons que la méthode « DoWithDelegate » attend en premier paramètre un objet du type « Action<string> ». En fait, « Action » est une substitution de « delegate ». Si nous faisons le parallélisme :

delegate DelegateName() = Action
delegate DelegateName(string arg) = Action<string>
delegate DelegateName(string arg, int i, double d) = Action<string, int, double&;gt

La transposition est assez simple et logique. Il faut savoir qu’en .NET 3.5, il n’existe que 5 types d’Action différents :

Action
Action<T>
Action<T1, T2>
Action<T1, T2, T3>
Action<T1, T2, T3, T4>

Autrement dit, un « delegate » attendant 5 arguments ne pourrait pas être placé dans un « delegate » du type « Action ». Un cas typique d’utilisation d’Action est SharePoint. Effectivement, pour ouvrir un site SharePoint via l’object model de celui-ci, nous devons créer un objet de type SPSite et un objet de type SPWeb. Cela ne pose pas de problème, mais c’est un peu contraignant de toujours devoir retaper le même bout de code pour faire la même chose. Par exemple :



Il n’y a pas des masses de code, mais nous pouvons simplifier cela de cette manière :



Nous déclarons donc une fonction « DoOnWeb » qui attend une url et un « Action » en paramètre. L’url contiendra l’url du site à ouvrir et « action » contiendra une référence à une méthode qu’il faudra exécuter en lui faisant passer l’objet « SPWeb » en paramètre. Nous déclarons donc l’objet SPSite et SPWeb et appelons le « delegate » en passant « web » en paramètre. Enfin, nous appelons cette méthode en indiquant que le « delegate » affichera en fait la propriété « Title » de l’objet « SPWeb » créé. Alors bien évidemment, vous allez me dire qu’il y a plus de ligne de code. C’est sûr, cependant, il faut penser à la réutilisation de cette fonction. Effectivement, en SharePoint, l’instanciation d’un objet SPSite et SPWeb sont très courant. Il nous est donc possible de réutiliser la fonction « DoOnWeb » pour nous éviter de toujours devoir répéter le code d’instanciation du site Web SharePoint. Au final, nous gagnons beaucoup de ligne de code.

Func



Si vous avez compris comment utiliser « Action », « Func » ne vous posera pas le moindre problème. Dans le cas contraire, nous vous conseillons de relire le début de ce cours pour être certain de bien comprendre « Func ».

La différence entre « Action » et « Func » réside dans le fait que nous pouvons définir une valeur de retour au « delegate » avec « Func », ce qui n’est pas possible avec « Action ». En effet :

Action<string> = delegate void DelegateName(string)

Mais :

Func<string, int> = delegate int DelegateName(string)

En effet, lorsque nous déclarons un « Func », le dernier type entre les chevrons (< >) désigne en fait le type de retour du « delegate ». Ainsi, si nous voulons déclarer un « delegate » respectant la signature suivante :



Il nous faudrait utiliser « Func » de cette manière :



De cette manière, le dernier type de « Func » sera considéré comme le type de retour du « delegate ». Voyez cet exemple :



Cette fois, « DoMethodWithDelegate » attend un « Func » en paramètre. Ce « delegate », quant à lui, attend 2 paramètres de type « int » et renvoit une valeur de type « bool ». Ensuite, nous utilisons ce « delegate » en lui faisant passer 2 nombres entrés par l’utilisateur et affichons « TRUE » si le « delegate » renvoie « true » et « FALSE » dans le cas contraire.

Nous appelons enfin cette méthode en disant que le « delegate » devra renvoyer « true » si les deux nombres passés en paramètres sont égaux. Si nous exécutons ce programme, nous obtenons ceci :

Image


Image


Comme vous le voyez, selon que les deux nombres sont égaux ou non, le message affiché dans la console est bien différent, le « delegate » est donc bien exécuté.

En C# 3.5, il existe 5 types de « Func » :

Func<TResult>
Func<T1, TResult>
Func<T1, T2, TResult>
Func<T1, T2, T3, TResult>
Func<T1, T2, T3, T4, TResult>

Voyons maintenant un exemple concret inspiré de la fonction « Where » de LINQ que nous allons simplifier pour l’exemple. Disons que votre projet contient cette classe :



Plus loin dans votre code, vous créez une liste contenant des étudiants :



Imaginons maintenant que nous voulons récupérer l’identité (nom + prénom) de tous les étudiants ayant moins de 30 ans, la technique traditionnelle serait :

var moinsDe30 = new List();

Cela fonctionnera correctement, mais si nous voulons juste après récupérer tous les étudiants portant le nom de « Skywalker », il nous faudrait faire un « foreach » suivi d’un « if ». Naturellement, vous pouvez vous dire qu’il suffit de créer une fonction, mais au final, le problème est le même, il faudra autant de fonction que de possibilité que le critère de sélection des étudiants change.

Cependant, l’idée de la fonction est la bonne piste, mais il faut en plus utiliser un « Func » :



Ici, nous créons une fonction qui va renvoyer une liste de « string ». Nous utilisons « IEnumerable » pour être « générique » (nous pourrions utiliser des types génériques, mais nous verrons cela dans un prochain cours). La fonction « GetEtudiants » attend comme premier paramètre une liste d’étudiants et comme deuxième paramètre un « delegate » qui attend un paramètre de type étudiant et qui renvoie un « booléen ».

Dans cette méthode, nous commençons par parcourir tous les éléments de la liste. Ensuite, nous utilisons le « delegate » pour tester une condition basée sur l’élément parcouru actuellement dans le « foreach ». Cette condition sera définie lors de l’appel à cette fonction, nous le verrons plus tard. Si la condition du « delegate » est respectée, nous faisons un « yield return » de la concaténation du nom et du prénom de l’étudiant. Pour ceux qui ne connaissent pas le « yield return », nous verrons à quoi il sert dans un autre cours. Pour l’instant, contentez-vous de savoir qu’il permet de constituer la liste retournée par la fonction.

Enfin, le plus important est la manière d’appeler cette fonction :



La première ligne va permettre de récupérer tous les étudiants ayant moins de 30 ans. Comme vous pouvez le constater, il suffit simplement de faire passer la condition dans le « delegate ». C’est cette condition qui sera vérifiée dans le « foreach » et qui permettra de sélectionner ou non l’élément. Le gros avantage réside dans le fait que peu importe le critère de sélection des étudiants, nous n’aurons à chaque fois qu’une ligne de code. La preuve avec la ligne suivante qui permet en une seule ligne de récupérer tous les étudiants portant le nom « Skywalker ».

Télécharger les sources de l'exemple

Voter :

4 commentaires

  • jeje a dit:

    21/05/2012

    Génial :)

  • Said a dit:

    18/01/2012

    Magnifique Tuto, clair et précis comme d'habitude.

  • Cyril a dit:

    30/12/2011

    Très bon tutoriel, ça m'a aider à comprendre les expressions lambda!

  • Marc13100 a dit:

    08/11/2011

    bon tuto, je connaissais pas , merci,

Ajouter un commentaire