Expressions lambda et fonctions anonymes
Vous utilisez une expression lambda pour créer une fonction anonyme. Utilisez l’opérateur de déclaration lambda =>
pour séparer la liste des paramètres lambda de son corps. Une expression lambda peut être de l’une des deux formes suivantes :
Expression lambda qui a une expression comme corps :
(input-parameters) => expression
Instruction lambda qui a un bloc d’instructions comme corps :
(input-parameters) => { <sequence-of-statements> }
Pour créer une expression lambda, vous spécifiez des paramètres d’entrée (le cas échéant) sur le côté gauche de l’opérateur lambda et une expression ou un bloc d’instructions de l’autre côté.
Toute expression lambda peut être convertie en un délégué de type . Les types de ses paramètres et valeur de retour définissent le type délégué vers lequel une expression lambda peut être convertie. Si une expression lambda ne retourne pas de valeur, elle peut être convertie en un des types délégués Action
; sinon, il peut être converti en un des types délégués Func
. Par exemple, une expression lambda ayant deux paramètres et ne retournant aucune valeur peut être convertie en délégué Action<T1,T2>. Une expression lambda qui a un paramètre et retourne une valeur peut être convertie en délégué Func<T,TResult>. Dans l’exemple suivant, l’expression lambda x => x * x
, qui spécifie un paramètre nommé x
et retourne la valeur de x
carré, est affectée à une variable d’un type délégué :
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
Les expressions lambda peuvent également être converties en types d'arbres d'expressions , comme l’illustre l’exemple suivant :
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
Vous utilisez des expressions lambda dans n’importe quel code qui nécessite des instances de types délégués ou d’arborescences d’expressions. L’un des exemples est l’argument de la méthode Task.Run(Action) pour passer le code qui doit être exécuté en arrière-plan. Vous pouvez également utiliser des expressions lambda lorsque vous écrivez LINQ en C#, comme l’illustre l’exemple suivant :
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
Lorsque vous utilisez la syntaxe basée sur des méthodes pour appeler la méthode Enumerable.Select dans la classe System.Linq.Enumerable, par exemple dans LINQ to Objects et LINQ to XML, le paramètre est un type délégué System.Func<T,TResult>. Lorsque vous appelez la méthode Queryable.Select dans la classe System.Linq.Queryable, par exemple dans LINQ to SQL, le type de paramètre est un type d’arborescence d’expressions Expression<Func<TSource,TResult>>
. Dans les deux cas, vous pouvez utiliser la même expression lambda pour spécifier la valeur du paramètre. Cela rend les deux appels Select
similaires, alors que le type d'objets créés par les lambdas est en fait différent.
Expressions lambdas
Une expression lambda avec une expression sur le côté droit de l’opérateur =>
est appelée expression lambda . Une expression lambda retourne le résultat de l’expression et prend la forme de base suivante :
(input-parameters) => expression
Le corps d’une expression lambda peut se composer d’un appel de méthode. Toutefois, si vous créez des arborescences d’expressions qui sont évaluées en dehors du contexte du CLR (Common Language Runtime) .NET, comme dans SQL Server, vous ne devriez pas utiliser d’appels de méthodes dans les expressions lambda. Les méthodes n’ont aucune signification en dehors du contexte du Common Language Runtime (CLR) .NET.
Instructions lambda
Une instruction lambda ressemble à une expression lambda, sauf que ses instructions sont placées entre accolades :
(input-parameters) => { <sequence-of-statements> }
Le corps d’une instruction lambda peut se composer de n’importe quel nombre d’instructions ; toutefois, dans la pratique, il n’y a généralement pas plus de deux ou trois.
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
Vous ne pouvez pas utiliser des instructions lambda pour créer des arborescences d’expressions.
Paramètres d’entrée d’une expression lambda
Vous placez les paramètres d’entrée d’une expression lambda entre parenthèses. Spécifiez les paramètres d’entrée zéro avec des parenthèses vides :
Action line = () => Console.WriteLine();
Si une expression lambda n’a qu’un seul paramètre d’entrée, les parenthèses sont facultatives :
Func<double, double> cube = x => x * x * x;
Deux paramètres d’entrée ou plus sont séparés par des virgules :
Func<int, int, bool> testForEquality = (x, y) => x == y;
Parfois, le compilateur ne peut pas déduire les types de paramètres d’entrée. Vous pouvez spécifier les types explicitement comme indiqué dans l’exemple suivant :
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
Les types de paramètres d’entrée doivent être explicites ou implicites ; sinon, une erreur de compilateur CS0748 se produit.
Vous pouvez utiliser ignorer pour spécifier deux paramètres d’entrée ou plus d’une expression lambda qui ne sont pas utilisés dans l’expression :
Func<int, int, int> constant = (_, _) => 42;
Les paramètres d’abandon lambda peuvent être utiles lorsque vous utilisez une expression lambda pour fournir un gestionnaire d’événements.
Remarque
Pour la compatibilité descendante, si un seul paramètre d’entrée est nommé _
, dans une expression lambda, _
est traité comme le nom de ce paramètre.
À partir de C# 12, vous pouvez fournir des valeurs par défaut pour les paramètres des expressions lambda. La syntaxe et les restrictions relatives aux valeurs de paramètres par défaut sont identiques aux méthodes et aux fonctions locales. L’exemple suivant déclare une expression lambda avec un paramètre par défaut, puis l’appelle une fois à l’aide de la valeur par défaut et une fois avec deux paramètres explicites :
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
Vous pouvez également déclarer des expressions lambda avec des tableaux params
ou des collections comme paramètres :
var sum = (params IEnumerable<int> values) =>
{
int sum = 0;
foreach (var value in values)
sum += value;
return sum;
};
var empty = sum();
Console.WriteLine(empty); // 0
var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15
Dans le cadre de ces mises à jour, lorsqu’un groupe de méthodes qui a un paramètre par défaut est affecté à une expression lambda, cette expression lambda a également le même paramètre par défaut. Un groupe de méthodes avec un paramètre de collection params
peut également être affecté à une expression lambda.
Les expressions lambda avec des paramètres par défaut ou des collections params
comme paramètres n'ont pas de types naturels correspondant aux types Func<>
ou Action<>
. Toutefois, vous pouvez définir des types délégués qui incluent des valeurs de paramètre par défaut :
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
Vous pouvez également utiliser des variables implicitement typées avec des déclarations var
pour définir le type délégué. Le compilateur synthétise le type de délégué approprié.
Pour plus d’informations sur les paramètres par défaut sur les expressions lambda, consultez la spécification de fonctionnalité pour paramètres par défaut sur les expressions lambda.
Lambdas asynchrones
Vous pouvez facilement créer des expressions et des instructions lambda qui incorporent le traitement asynchrone à l'aide des mots clés async et await . Par exemple, l’exemple Windows Forms suivant contient un gestionnaire d’événements qui appelle et attend une méthode asynchrone, ExampleMethodAsync
.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
Vous pouvez ajouter le même gestionnaire d’événements à l’aide d’un lambda asynchrone. Pour ajouter ce gestionnaire, ajoutez un modificateur async
avant la liste des paramètres lambda, comme l’illustre l’exemple suivant :
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
Pour plus d’informations sur la création et l’utilisation des méthodes asyncrones, consultez Programmation asynchrone avec async et await.
Expressions lambda et tuples
Le langage C# fournit un support intégré pour les tuples . Vous pouvez fournir un tuple en tant qu’argument à une expression lambda, et votre expression lambda peut également retourner un tuple. Dans certains cas, le compilateur C# utilise l’inférence de type pour déterminer les types de composants tuple.
Vous définissez un tuple en englobant une liste délimitée par des virgules de ses composants entre parenthèses. L’exemple suivant utilise le tuple avec trois composants pour passer une séquence de nombres à une expression lambda, qui double chaque valeur et retourne un tuple avec trois composants qui contiennent le résultat des multiplications.
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
En règle générale, les champs d’un tuple sont nommés Item1
, Item2
, et ainsi de suite. Toutefois, vous pouvez définir un tuple avec des composants nommés, comme l’exemple suivant.
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
Pour plus d’informations sur les tuples C#, consultez les types de Tuple.
Lambdas avec les opérateurs de requête standard
LINQ to Objects, entre autres implémentations, a un paramètre d'entrée dont le type est l'un des délégués génériques de la famille Func<TResult>. Ces délégués utilisent des paramètres de type pour définir le nombre et le type de paramètres d’entrée, ainsi que le type de retour du délégué. Les délégués Func
sont utiles pour encapsuler des expressions définies par l’utilisateur qui sont appliquées à chaque élément dans un ensemble de données sources. Par exemple, considérez le type de délégué Func<T,TResult> :
public delegate TResult Func<in T, out TResult>(T arg)
Le délégué peut être instancié en tant qu’instance Func<int, bool>
où int
est un paramètre d’entrée et bool
est la valeur de retour. La valeur de retour est toujours spécifiée dans le dernier paramètre de type. Par exemple, Func<int, string, bool>
définit un délégué avec deux paramètres d’entrée, int
et string
, et un type de retour de bool
. Le délégué Func
suivant, lorsqu’il est appelé, retourne une valeur booléenne qui indique si le paramètre d’entrée est égal à cinq :
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
Vous pouvez également fournir une expression lambda lorsque le type d’argument est un Expression<TDelegate>, par exemple dans les opérateurs de requête standard définis dans le type Queryable. Lorsque vous spécifiez un argument Expression<TDelegate>, l’expression lambda est compilée dans une arborescence d’expressions.
L’exemple suivant utilise l’opérateur de requête standard Count :
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
Le compilateur peut déduire le type du paramètre d’entrée, ou vous pouvez également le spécifier explicitement. Cette expression lambda particulière compte ces entiers (n
) qui, lorsqu’elles sont divisées par deux, ont un reste de 1.
L’exemple suivant génère une séquence qui contient tous les éléments du tableau numbers
qui précèdent les 9, car c’est le premier nombre de la séquence qui ne répond pas à la condition :
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
L’exemple suivant spécifie plusieurs paramètres d’entrée en les plaçant entre parenthèses. La méthode retourne tous les éléments du tableau numbers
jusqu’à ce qu’il trouve un nombre dont la valeur est inférieure à sa position ordinale dans le tableau :
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4
Vous n’utilisez pas d’expressions lambda directement dans expressions de requête, mais vous pouvez les utiliser dans les appels de méthode dans les expressions de requête, comme l’illustre l’exemple suivant :
var numberSets = new List<int[]>
{
new[] { 1, 2, 3, 4, 5 },
new[] { 0, 0, 0 },
new[] { 9, 8 },
new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};
var setsWithManyPositives =
from numberSet in numberSets
where numberSet.Count(n => n > 0) > 3
select numberSet;
foreach (var numberSet in setsWithManyPositives)
{
Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0
Inférence de type dans les expressions lambda
Lorsque vous écrivez des lambdas, vous n’avez souvent pas besoin de spécifier un type pour les paramètres d’entrée, car le compilateur peut déduire le type en fonction du corps lambda, des types de paramètres et d’autres facteurs, comme décrit dans la spécification du langage C#. Pour la plupart des opérateurs de requête standard, la première entrée est le type des éléments de la séquence source. Si vous interrogez un IEnumerable<Customer>
, la variable d’entrée est déduite comme un objet Customer
, ce qui signifie que vous avez accès à ses méthodes et propriétés :
customers.Where(c => c.City == "London");
Les règles générales pour l’inférence de type pour les lambdas sont les suivantes :
- L’expression lambda doit contenir le même nombre de paramètres que le type délégué.
- Chaque paramètre d’entrée de l’lambda doit être implicitement convertible en son paramètre délégué correspondant.
- La valeur de retour du lambda (le cas échéant) doit être implicitement convertible en type de retour du délégué.
Type naturel d’une expression lambda
Une expression lambda en soi n’a pas de type, car le système de type commun n’a pas de concept intrinsèque d'« expression lambda ». Toutefois, il est parfois pratique de parler de manière informelle du « type » d’une expression lambda. Ce « type » informel fait référence au type délégué ou Expression type vers lequel l’expression lambda est convertie.
Une expression lambda peut avoir un type naturel. Au lieu de vous forcer à déclarer un type délégué, tel que Func<...>
ou Action<...>
pour une expression lambda, le compilateur peut déduire le type délégué de l’expression lambda. Par exemple, considérez la déclaration suivante :
var parse = (string s) => int.Parse(s);
Le compilateur peut déduire parse
être un Func<string, int>
. Le compilateur choisit un délégué Func
ou Action
disponible, le cas échéant. Sinon, il synthétise un type délégué. Par exemple, le type délégué est synthétisé si l’expression lambda a ref
paramètres. Lorsqu’une expression lambda a un type naturel, elle peut être affectée à un type moins explicite, tel que System.Object ou System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
Les groupes de méthodes (autrement dit, les noms de méthodes sans listes de paramètres) avec exactement une surcharge ont un type naturel :
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
Si vous affectez une expression lambda à System.Linq.Expressions.LambdaExpression, ou System.Linq.Expressions.Expression, et que l’expression lambda a un type délégué naturel, l’expression a un type naturel de System.Linq.Expressions.Expression<TDelegate>, avec le type délégué naturel utilisé comme argument pour le paramètre de type :
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Toutes les expressions lambda n’ont pas de type naturel. Tenez compte de la déclaration suivante :
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
Le compilateur ne peut pas déduire un type de paramètre pour s
. Lorsque le compilateur ne peut pas déduire un type naturel, vous devez déclarer le type :
Func<string, int> parse = s => int.Parse(s);
Type de retour explicite
En règle générale, le type de retour d’une expression lambda est évident et déduit. Pour certaines expressions qui ne fonctionnent pas :
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
Vous pouvez spécifier le type de retour d’une expression lambda avant les paramètres d’entrée. Lorsque vous spécifiez un type de retour explicite, vous devez parenthèser les paramètres d’entrée :
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Attributs
Vous pouvez ajouter des attributs à une expression lambda et à ses paramètres. L’exemple suivant montre comment ajouter des attributs à une expression lambda :
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
Vous pouvez également ajouter des attributs aux paramètres d’entrée ou à la valeur de retour, comme l’illustre l’exemple suivant :
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
Comme le montrent les exemples précédents, vous devez parenthèser les paramètres d’entrée lorsque vous ajoutez des attributs à une expression lambda ou à ses paramètres.
Important
Les expressions lambda sont appelées par le biais du type de délégué sous-jacent. C’est différent des méthodes et des fonctions locales. La méthode Invoke
du délégué ne vérifie pas les attributs sur l’expression lambda. Les attributs n’ont aucun effet lorsque l’expression lambda est appelée. Les attributs sur les expressions lambda sont utiles pour l’analyse du code et peuvent être découverts via la réflexion. L’une des conséquences de cette décision est que la System.Diagnostics.ConditionalAttribute ne peut pas être appliquée à une expression lambda.
Capture des variables externes et de l’étendue des variables dans les expressions lambda
Les lambda peuvent faire référence à des variables externes . Ces variables externes sont les variables qui sont dans le périmètre de la méthode qui définit l’expression lambda ou dans le périmètre du type qui contient l’expression lambda. Les variables capturées de cette manière sont stockées pour une utilisation dans l'expression lambda, même si les variables se trouvent en dehors de la portée et sont récupérées par le garbage collector. Une variable externe doit être définitivement affectée avant de pouvoir être consommée dans une expression lambda. L’exemple suivant illustre ces règles :
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int>? updateCapturedLocalVariable;
internal Func<int, bool>? isEqualToCapturedLocalVariable;
public void Run(int input)
{
int j = 0;
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable!(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable!(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
Les règles suivantes s’appliquent à l’étendue des variables dans les expressions lambda :
- Une variable capturée ne sera pas collectée par le garbage collector tant que le délégué qui le référence devient éligible pour la garbage collection.
- Les variables introduites dans une expression lambda ne sont pas visibles dans la méthode englobante.
- Une expression lambda ne peut pas capturer directement un paramètre in, ref ou out à partir de la méthode englobante.
- Une instruction return dans une expression lambda ne provoque pas le retour de la méthode englobante.
- Une expression lambda ne peut pas contenir une instruction goto, breakou continue si la cible de cette instruction de saut se trouve en dehors du bloc de l'expression lambda. Il est également incorrect d’avoir une instruction de saut en dehors du bloc de l’expression lambda si la cible se trouve à l’intérieur du bloc.
Vous pouvez appliquer le modificateur static
à une expression lambda pour empêcher la capture involontaire de variables locales ou d’état d’instance par l’expression lambda :
Func<double, double> square = static x => x * x;
Une lambda statique ne peut pas capturer les variables locales ou l’état d’instance à partir d’étendues englobantes, mais peut référencer des membres statiques et des définitions constantes.
Spécification du langage C#
Pour plus d’informations, consultez la section expressions de fonction anonyme de la spécification du langage C# .
Pour plus d’informations sur ces fonctionnalités, consultez les notes de proposition de fonctionnalités suivantes :