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 de l’expression 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) à 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 type délégué. Le type délégué vers lequel une expression lambda peut être convertie est défini par les types de ses paramètres et de sa valeur de retour. Si une expression lambda ne retourne pas de valeur, elle peut être convertie en l’un des types délégués Action ; sinon, elle peut être convertie en l’un des types délégués Func. Par exemple, une expression lambda qui a deux paramètres et qui ne retourne aucune valeur peut être convertie en un délégué Action<T1,T2>. Une expression lambda qui a un paramètre et qui retourne une valeur peut être convertie en un 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 du carré de x, est assigné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’arborescence d’expression, comme le montre l’exemple suivant :

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)

Vous pouvez utiliser des expressions lambda dans tout code qui requiert des instances de types délégués ou d’arborescences d’expressions, par exemple en tant qu’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 la méthode Enumerable.Select est appelée avec une syntaxe fondée sur une méthode 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>. Si la méthode Queryable.Select est appelée dans la classe System.Linq.Queryable (par exemple, dans LINQ to SQL), le paramètre est un type d’arborescence d’expression Expression<Func<TSource,TResult>>. Dans les deux cas, vous pouvez utiliser la même expression lambda pour spécifier la valeur de paramètre. Les deux appels Select semblent ainsi semblables alors qu’en fait le type des objets créés à partir des lambdas est différent.

Expressions lambdas

Une expression lambda comportant une expression à droite 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. Cependant, si vous créez des arborescences d’expressions évaluées en dehors du contexte du Common Language Runtime (CLR) .NET, comme dans SQL Server, vous ne devez pas utiliser d’appels de méthode dans les expressions lambda. Les méthodes n’ont aucune signification en dehors du contexte du CLR (Common Language Runtime) .NET.

Instructions lambda

Une instruction lambda ressemble à une expression lambda, mais l'instruction ou les instructions sont mises entre accolades :

(input-parameters) => { <sequence-of-statements> }

Le corps d'une instruction lambda peut se composer d'un nombre illimité d'instructions ; toutefois, en pratique, leur nombre est généralement 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 des paramètres d'entrée de 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 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 tous explicites ou tous implicites ; sinon, une erreur de compilateur CS0748 se produit.

Vous pouvez utiliser des abandons 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.

Notes

Pour la compatibilité descendante, si un seul paramètre d’entrée est nommé _, puis, 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ètre par défaut sont les mêmes que pour les méthodes et 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 en tant que paramètres :

var sum = (params 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, quand 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 tableau params peut également être affecté à une expression lambda.

Les expressions lambda avec des paramètres par défaut ou des tableaux params en tant que paramètres n’ont pas de types naturels qui correspondent 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);

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 délégué approprié.

Pour plus d’informations, consultez la spécification de fonctionnalités des paramètres par défaut pour 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 async 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 en utilisant une expression lambda asynchrone. Pour ajouter ce gestionnaire, ajoutez un modificateur async avant la liste des paramètres lambda, comme dans 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 une prise en charge intégrée des tuples. Vous pouvez fournir un tuple comme argument à une expression lambda, et votre expression lambda peut aussi retourner un tuple. Dans certains cas, le compilateur C# utilise l’inférence de type pour déterminer les types des composants du tuple.

Vous définissez un tuple en plaçant entre des parenthèses une liste de ses composants avec des virgules comme séparateur. L’exemple suivant utilise un tuple à trois composants pour passer une séquence de nombres à une expression lambda, qui double chaque valeur et retourne un tuple à trois composants contenant 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. Vous pouvez cependant définir un tuple avec des composants nommés, comme dans 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, parmi d’autres implémentations, a un paramètre d’entrée dont le type fait partie de la famille Func<TResult> de délégués génériques. Ces délégués utilisent des paramètres de type pour définir le nombre et le type des 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. Prenons par exemple le type délégué Func<T,TResult> :

public delegate TResult Func<in T, out TResult>(T arg)

Ce délégué peut être instancié comme une instance Func<int, bool>, où int est un paramètre d’entrée et bool 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 bool. Le délégué Func suivant, lorsqu’il est appelé, retourne une valeur booléenne indiquant 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 aussi fournir une expression lambda quand le type d’argument est Expression<TDelegate>, par exemple, dans les opérateurs de requête standard définis dans le type Queryable. Quand vous spécifiez un argument Expression<TDelegate>, l’expression lambda est compilée en arborescence de l’expression.

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 les entiers (n) qui, lorsqu'ils sont divisés par deux, ont un reste de 1.

L’exemple suivant produit une séquence qui contient tous les éléments du tableau numbers précédant le 9, car c’est le premier nombre de la séquence qui ne remplit 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 les 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 et expressions lambda

Il n’est généralement pas nécessaire, avec les expressions lambda, de spécifier un type pour les paramètres d’entrée, car le compilateur peut le déduire du corps de l’expression, des types de paramètres et d’autres facteurs, comme le décrit 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 dans 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");

Voici les règles générales de l’inférence de type pour les expressions lambda :

  • Le lambda doit contenir le même nombre de paramètres que le type délégué.
  • Chaque paramètre d'entrée dans le lambda doit être implicitement convertible en son paramètre de 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 au type Expression vers lequel l’expression lambda est convertie.

À compter de C# 10, une expression lambda peut avoir un typenaturel. 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. Observez par exemple la déclaration suivante :

var parse = (string s) => int.Parse(s);

Le compilateur peut déduire que parse est un Func<string, int>. Le compilateur choisit un délégué Func ou Action disponible, s’il en existe un. Sinon, il synthétise un type délégué. Par exemple, le type délégué est synthétisé si l’expression lambda a des paramètres ref. 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. Prenons 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

À compter de C#10, 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 mettre entre parenthèses les paramètres d’entrée :

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

Attributs

À compter de C#10, 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 mettre entre parenthèses 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 System.Diagnostics.ConditionalAttribute ne peut pas être appliqué à une expression lambda.

Capture des variables externes et de l’étendue variable dans les expressions lambda

Les expressions lambda peuvent faire référence à des variables externes. Ces variables externes sont des variables dans l’étendue de la méthode qui définit l’expression lambda, ou dans l’étendue 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 assignée de manière précise pour pouvoir être utilisé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 à la portée 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 d’instruction goto, breakou continue si la cible de cette instruction jump se trouve en dehors du bloc d’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 expression 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 des fonctionnalités suivantes :

Voir aussi