1. Introduction
Jusqu'ici vous avez vu : nous appelions des méthodes LINQ qui prenaient une lambda en entrée. Mais comment une méthode C# sait-elle qu'on peut lui passer une fonction ?
Pour faire simple, les delegates sont comme une interface pour les fonctions : on décrit la signature (types des paramètres et valeur de retour), et n'importe quelle méthode (et lambda !) correspondant à cette signature peut être passée là où on attend ce delegate.
Souvenez-vous comment vous passiez une string comme paramètre ? C'est à peu près la même idée avec la « logique », sauf que le type du paramètre est un delegate.
Delegates : théorie de base en langage clair
En C#, un delegate est un type qui décrit « une fonction avec telle signature ».
// Delegate qui prend un int et retourne un bool
public delegate bool IntPredicate(int x);
Toute fonction compatible par la signature peut être assignée à une variable de ce type :
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
Et maintenant — une lambda convient :
IntPredicate pred = x => x % 2 == 0;
Delegates génériques : Func, Action, Predicate
- Func<T1, ..., TResult> — une fonction qui prend des paramètres T1, ... et qui retourne TResult.
- Action<T1, ...> — une fonction qui prend des paramètres et ne retourne rien (void).
- Predicate<T> — une fonction qui prend un T et retourne un bool.
2. Passer une lambda à votre méthode
Imaginons que nous développons notre mini-application d'apprentissage — un projet console qui travaille avec une liste d'utilisateurs. Avant nous filtrions les collections via LINQ, et maintenant nous allons écrire notre propre méthode qui accepte une condition sous forme de lambda.
Créons notre méthode avec un paramètre lambda
// Définissons la classe User pour l'exemple (ajoutons-la à notre application)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// Méthode qui prend une liste et un delegate-condition (lambda)
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // On appelle la lambda !
result.Add(user);
}
return result;
}
Maintenant on peut passer n'importe quelle lambda :
var users = new List<User>
{
new User { Name = "Vasya", IsActive = true },
new User { Name = "Petya", IsActive = false },
new User { Name = "Masha", IsActive = true }
};
// On filtre seulement les actifs
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // Vasya, Masha
Et voilà ! On a passé un morceau de logique — une mini-fonction — comme paramètre ordinaire, simplement parce que la méthode FilterUsers attend un Predicate<User>, et on lui a donné une lambda compatible.
Variantes avec Func<T, TResult>
Predicate<T> est approprié quand il faut une condition (retourne bool). Et si on veut « calculer » quelque chose pour chaque utilisateur ?
// Méthode qui applique une fonction à chaque élément et collecte les résultats
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
Utilisation :
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // VASYA, PETYA, MASHA
3. Détails utiles
Différentes formes de passage
On peut passer non seulement une lambda, mais aussi une méthode ordinaire — l'essentiel est que la signature corresponde.
// Méthode ordinaire
static bool NameHasS(User user) => user.Name.Contains("s");
// Passage d'une méthode ordinaire :
var usersWithS = FilterUsers(users, NameHasS);
// Passage d'une lambda
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
On peut aussi utiliser une méthode anonyme à l'ancien style (mais évitez) :
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
Le style moderne — c'est les lambdas !
Passer des lambdas à LINQ : ce qui se passe vraiment
var result = users.Where(u => u.IsActive).ToList();
Sous le capot, Where prend un Func<User, bool>. Ça veut dire que n'importe quelle méthode qui attend un Func<...> peut être utilisée de la même manière !
Et si on veut deux paramètres ?
// La méthode prend deux lambdas pour filtrer
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
Utilisation :
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("V"),
u => u.IsActive == false
);
// Prendra seulement les utilisateurs dont le nom commence par "V" et qui sont actifs
Scénario : Fabrique de filtres
Console.WriteLine("Entrez la longueur minimale du nom :");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// Assez interactif et vivant !
4. Erreurs typiques et subtilités
Parfois le compilateur ne peut pas « inférer » les types des paramètres d'une lambda — surtout dans des scénarios complexes avec des surcharges ou quand la méthode attend un delegate avec plusieurs paramètres/un type de retour concret. Dans ce cas, on peut préciser explicitement les types de la lambda :
FilterUsers(users, (User u) => u.Name.Length > 3);
ou même :
MapUsers(users, (User u) => u.Name.ToUpper());
Erreur : la lambda ne correspond pas à la signature
FilterUsers(users, user => Console.WriteLine(user.Name)); // erreur ! On attend bool, on obtient void
Parce qu'on attend une fonction qui retourne bool, et la lambda retourne void (plus précisément, ne retourne rien explicitement). Faites attention au type de retour !
Erreur : abus de lambdas
Si vous commencez à passer des lambdas de 10 lignes, mieux vaut les sortir dans une méthode séparée. C'est plus lisible et le debug est plus simple.
GO TO FULL VERSION