1. Introduction
Dans beaucoup de projets C# modernes il n'y a presque plus de méthodes "classiques" pour gérer les événements. C'est arrivé parce que les expressions lambda permettent de déclarer rapidement et de façon concise un handler directement à l'endroit de l'abonnement (opérateur +=), si le traitement est simple et n'est pas réutilisé ailleurs. C'est comme coller un petit post-it directement sur la machine à café avec la consigne «appuie ce bouton» au lieu d'écrire un manuel détaillé et le stocker dans un dossier séparé. Si la tâche est locale et ponctuelle, la lambda est idéale !
Où c'est utilisé en pratique
- Dans ASP.NET (par exemple, pour gérer les événements du cycle de vie d'une page),
- En WPF/WinForms pour l'UI (par exemple, au clic sur un bouton),
- En programmation serveur (par exemple, logique à l'intérieur d'un pipeline),
- Dans les tests, quand le handler n'a pas besoin d'un nom séparé.
Syntaxe : à quoi ça ressemble dans le code
Regardons d'abord la gestion classique d'un événement :
// Déclaration de l'événement
public event EventHandler? MyEvent;
// Abonnement à l'événement avec une méthode classique
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("L'événement s'est produit !");
}
public void Subscribe()
{
MyEvent += Handler;
}
Maintenant — la version avec une expression lambda (fonction anonyme) :
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("L'événement s'est produit (lambda) !");
}
Remarquez : on ne crée pas de méthode séparée, on définit le handler directement lors de l'abonnement. La signature de la lambda correspond automatiquement au type de l'événement (EventHandler).
Comparaison des approches
| Méthode classique | Lambda | |
|---|---|---|
| Volume de code | Plus (méthode + abonnement) | Moins, tout sur place |
| Réutilisabilité | Peut être réutilisée | Généralement non |
| Localité du code | Éparpillé | Tout proche |
| Clarté/lisibilité | Bon pour la logique complexe | Excellent pour les cas simples |
2. Application avec gestion d'événements et lambdas
Structure de base
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"Élément de menu {index} sélectionné.");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
Abonnement avec une expression lambda
class Program
{
static void Main()
{
var menu = new Menu();
// Abonnement à l'événement via une lambda
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("Merci pour votre choix ! Le handler lambda s'est déclenché.");
};
menu.SelectItem(1);
}
}
Sortie attendue :
Élément de menu 1 sélectionné.
Merci pour votre choix ! Le handler lambda s'est déclenché.
Capture de variables depuis le contexte externe
Un des avantages d'une expression lambda — elle peut "se souvenir" des valeurs depuis le scope externe (closures). Par exemple, compter combien de fois un élément de menu a été sélectionné : la variable counter sera capturée par la closure.
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"Élément sélectionné {counter} fois !");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
Sortie attendue :
Élément de menu 1 sélectionné.
Élément sélectionné 1 fois !
Élément de menu 2 sélectionné.
Élément sélectionné 2 fois !
C'est la magie des closures : la variable counter continue d'exister à l'intérieur de la lambda !
Exemple avec des paramètres d'événement
Si votre événement utilise EventHandler<T>, où T est une classe personnalisée avec des infos supplémentaires, la lambda s'adapte simplement à la signature requise.
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"Élément de menu {index} : {description} sélectionné.");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// Utilisation
static void Main()
{
var menu = new Menu();
// Lambda avec déballage des arguments de l'événement
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"Élément #{args.ItemIndex} sélectionné : {args.Description.ToUpper()}");
};
menu.SelectItem(3, "À propos");
}
Sortie attendue :
Élément de menu 3 : À propos sélectionné.
Élément #3 sélectionné : À PROPOS
3. Handlers locaux et lambda
Les lambdas sont idéales quand :
- La logique du handler est courte et claire,
- Le handler n'est utilisé qu'à un seul endroit,
- Il faut "capturer" des variables depuis le contexte local.
Si le traitement est complexe, nécessite de la réutilisation ou peut être appelé hors du lieu de déclaration, il est préférable d'utiliser une méthode nommée séparée.
Exemple : logique à l'intérieur et à l'extérieur
Lambda (idéal) :
button.Click += (s, e) => MessageBox.Show("Bouton cliqué !");
Méthodes (quand la logique est plus complexe ou réutilisable) :
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("Données sauvegardées !");
}
}
4. Sous le capot : que se passe-t-il avec les handlers lambda
Une lambda est aussi un delegate, comme un handler classique. Le compilateur crée "à la volée" une méthode anonyme, et s'il y a des variables capturées, il crée aussi une classe cachée pour les stocker.
Il est important de se rappeler (et c'est une erreur fréquente) : si vous créez une lambda dans une boucle et que vous l'abonnez à un événement, toutes les itérations peuvent capturer la même variable de boucle.
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Clic sur le bouton n°{i}");
};
}
Pour tous les handlers, le numéro du bouton peut finir par être égal à 5 ! Pour éviter ça, créez une copie de la variable à l'intérieur de la boucle :
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // copie locale
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Clic sur le bouton n°{buttonIndex}");
};
}
Maintenant tout fonctionne comme attendu.
La vraie puissance des handlers lambda : dans la vraie vie
Ces expressions lambda font gagner du temps et accélèrent le développement, surtout quand la logique est simple. Elles rapprochent le code de la tâche sans le disperser dans plusieurs fichiers et classes. Dans des projets réels vous les verrez partout : du traitement d'événements UI à l'abonnement aux messages dans des bus d'événements asynchrones.
5. Erreurs typiques
Erreur n°1 : oublier de se désabonner (-=) des objets longue durée.
Si un objet abonné ne se désabonne pas de l'événement de l'éditeur, la référence au delegate retient l'abonné en mémoire — le garbage collector ne pourra pas le collecter, même s'il n'y a plus d'autres références à l'objet. Cela provoque des "fuites" de mémoire et des dépendances pendantes, surtout quand l'éditeur vit longtemps (événements statiques, singletons, services).
Comment éviter : désabonnez-vous toujours lors de la libération/destruction (par exemple, Dispose, OnDisable, OnDestroy). Envisagez les références faibles (weak events / WeakEventManager), l'utilisation d'un pattern "gestionnaire d'événements" ou IObservable/Rx si le scénario est complexe.
Erreur n°2 : capturer des variables de la boucle sans copie locale.
Le piège typique — écrire l'abonnement à l'intérieur d'une boucle et la closure capture la même variable de boucle, donc tous les handlers voient la valeur finale, pas celle existant lors de la création du handler. Cela mène à des résultats inattendus (tous les handlers "impriment" le même nombre, etc.).
Comment éviter : créez une copie locale de la valeur à l'intérieur de la boucle et capturez-la :
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
Ou passez la valeur nécessaire à une méthode wrapper. C'est plus fiable et explicite sur l'intention.
Erreur n°3 : utiliser des lambdas volumineuses au point d'abonnement.
Si vous placez une logique métier massive directement dans une fonction anonyme lors de l'abonnement, le code devient difficile à lire, dur à tester et à déboguer. De plus — avec les lambdas anonymes il est plus compliqué de se désabonner correctement, car il faut le même instance de delegate. En conséquence, la logique se disperse dans le code et perd sa structure.
Comment éviter : extrayez la logique complexe dans des méthodes nommées ou des services ; si besoin, stockez le delegate dans une variable/champ pour pouvoir vous désabonner ; laissez dans la lambda seulement un petit wrapper/redirection. Cela améliorera la lisibilité et la maintenabilité du code.
GO TO FULL VERSION