Menu

Expressions lambda en C# : syntaxe, Func et Action, closures et LINQ

Comment fonctionnent les expressions lambda en C# : la syntaxe => sous toutes ses formes, stocker des lambdas dans Func et Action, les passer à des méthodes et à LINQ, les lambdas à instructions, les closures sur des variables capturées, et le bug de capture de la boucle for que foreach n'a pas.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Une expression lambda est une petite fonction anonyme écrite directement dans le code avec l'opérateur => (« va vers »). Les paramètres se placent à gauche, le résultat à droite :

Sortie :

60.0
12
ORDER SHIPPED!
96.0, 180.0

Une lambda n'a pas de type propre. Elle devient un délégué (un objet qui pointe vers une fonction) du type délégué qu'attend le contexte : ici Func<decimal, decimal>, un Predicate<T> pour List.FindAll, un EventHandler pour un événement. Les délégués Func renvoient une valeur et les délégués Action non ; la page Func et Action présente toute la famille.

Les formes de syntaxe

x => x * x                         // one parameter: parentheses optional
(x, y) => x + y                    // several parameters
() => DateTime.Now.Year            // no parameters
(int x, string s) => s.Length > x  // explicit parameter types
order =>                           // statement lambda: a block with return
{
    decimal subtotal = order.Qty * order.Price;
    return subtotal > 100m ? subtotal * 0.9m : subtotal;
}

Une lambda d'expression a une seule expression après =>, et sa valeur est le résultat. Une lambda à instructions a un bloc entre accolades, qui peut contenir n'importe quelles instructions et doit renvoyer une valeur avec return sur chaque chemin si le délégué en renvoie une (sinon CS1643, Not all code paths return a value in lambda expression).

Les versions récentes ont ajouté d'autres formes, toutes facultatives :

  • C# 9 : les paramètres discard, pour que plusieurs paramètres inutilisés puissent tous s'appeler _ ((_, _) => Save()), et les lambdas static, auxquelles il est interdit de capturer des variables.
  • C# 10 : un « type naturel », si bien que var square = (int x) => x * x; déduit Func<int, int>. Avant cela, var f = x => x * 2; échoue ; même en C# 10, cela échoue toujours sans type de paramètre, avec CS8917 The delegate type could not be inferred.
  • C# 12 : les valeurs par défaut des paramètres, (int x, int y = 1) => x + y.

Passer des lambdas à des méthodes

Toute méthode qui prend un paramètre délégué accepte une lambda. C'est ainsi que l'on passe un comportement, et pas seulement des données :

Sortie :

report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf

Filter ne sait pas ce que signifie « garder » ; l'appelant en décide avec une lambda. List.Sort fonctionne de la même façon : la lambda est une comparaison qui renvoie un nombre négatif, zéro ou positif, et Sort l'appelle aussi souvent que nécessaire.

Les lambdas dans LINQ

LINQ est l'endroit où la plupart du code C# rencontre les lambdas. Chaque opérateur de requête en prend une : Where une condition, Select une projection, OrderBy une clé, Sum une valeur à additionner :

Sortie :

Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45

Les lambdas ne s'exécutent pas là où elles sont écrites. LINQ les stocke et les appelle quand la requête est énumérée par foreach, ToList() ou Sum(), ce qu'on appelle l'exécution différée. La page LINQ couvre les opérateurs eux-mêmes.

Closures : les variables capturées

Une lambda peut utiliser les variables locales et les paramètres de la méthode dans laquelle elle est écrite. Elle capture la variable, pas sa valeur actuelle, et la variable survit aussi longtemps que la lambda :

Sortie :

1
2
3
1
50.0

Chaque appel à MakeCounter crée un nouveau count, donc les deux compteurs sont indépendants. La lambda de remise a lu rate au moment de son exécution, pas au moment où elle a été écrite, elle a donc appliqué 50 %. Le compilateur implémente cela en déplaçant les variables capturées dans une classe cachée que la méthode et la lambda partagent.

Le piège de la capture dans une boucle

Capturer une variable de boucle est l'endroit où les closures mordent. Une boucle for a une seule variable pour toute la boucle, donc chaque lambda créée à l'intérieur partage cette variable :

Sortie :

3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach

Les trois lambdas de la première boucle se sont exécutées après la fin de la boucle, quand i valait 3. Copier i dans une variable déclarée dans le corps donne à chaque lambda la sienne. foreach fait les choses correctement tout seul : depuis C# 5, sa variable d'itération est une nouvelle variable à chaque passage. Avant C# 5, il avait le même bug, c'est pourquoi d'anciennes réponses en ligne recommandent encore la copie pour foreach aussi.

Le même piège s'applique à tout ce qui s'exécute plus tard : les gestionnaires d'événements, les timers, Task.Run, et les requêtes LINQ construites dans une boucle et énumérées après.

Lambdas, méthodes anonymes et fonctions locales

Les lambdas ont remplacé la syntaxe des méthodes anonymes de C# 2, qui compile toujours :

Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; };   // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0;                          // lambda

Pour une fonction auxiliaire utilisée seulement dans une méthode, C# 7.0 propose aussi les fonctions locales, qui ont un nom, peuvent être récursives et n'allouent pas de délégué sauf si vous les convertissez en délégué. Utilisez une lambda quand vous passez un comportement à quelque chose (LINQ, Sort, un événement) ; utilisez une fonction locale ou une méthode privée quand vous l'appelez vous-même.

Une autre distinction apparaît avec les fournisseurs LINQ comme Entity Framework : une lambda affectée à Expression<Func<T, bool>> n'est pas du tout compilée en code, mais en une structure de données décrivant le code, que le fournisseur traduit en SQL. La syntaxe est identique ; c'est le type de paramètre de la méthode appelée qui décide de ce que vous obtenez.

Questions fréquentes

Qu'est-ce qu'une expression lambda en C# ?

Une fonction anonyme écrite avec l'opérateur => : x => x * 2 prend x et renvoie x * 2. Une lambda n'a pas de nom propre ; elle est convertie en un type délégué comme Func<int, int> ou Action<string> et peut être stockée dans une variable, passée à une méthode ou renvoyée par une méthode.

Comment écrire une lambda avec plusieurs paramètres ou sans paramètre ?

Mettez les paramètres entre parenthèses : (a, b) => a + b pour deux, () => DateTime.Now pour aucun. Seul un paramètre unique peut se passer des parenthèses (x => x + 1). Vous pouvez aussi indiquer les types explicitement : (int a, int b) => a + b.

Qu'est-ce qu'une closure dans une lambda C# ?

Une lambda qui utilise une variable locale de la méthode qui l'entoure capture cette variable, pas un instantané de sa valeur. La variable vit aussi longtemps que la lambda, et si l'un des deux côtés la modifie plus tard, l'autre voit la modification. C'est ce qui fait fonctionner les compteurs et les callbacks, et ce qui cause le bug classique de capture dans une boucle for.

Pourquoi les lambdas d'une boucle for voient-elles toutes la même valeur ?

Une boucle for a une seule variable de boucle pour toute la boucle, donc chaque lambda créée à l'intérieur capture le même i et voit sa valeur finale quand elle s'exécute plus tard. Copiez-la dans une variable locale à l'intérieur du corps (int copy = i;) et capturez la copie. foreach n'a pas ce problème depuis C# 5, car il crée une nouvelle variable à chaque itération.

Pourquoi var f = x => x * 2 ne compile-t-il pas ?

Le compilateur ne peut pas savoir quel type délégué vous voulez ni quel est le type de x, il signale donc CS8917 The delegate type could not be inferred (avant C# 10, CS0815). Déclarez le type (Func<int, int> f = x => x * 2;) ou, depuis C# 10, typez le paramètre : var f = (int x) => x * 2;.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER