1. Introduction
En programmation, un closure — ce n'est pas une façon de fermer une porte en JavaScript, mais un mécanisme où une expression lambda ou une méthode anonyme capture des variables du contexte environnant et « se souvient » d'elles même après la fin du bloc où elles ont été déclarées. En gros, un closure est une fonction qui a mémorisé les conditions dans lesquelles elle est née, et garde ces valeurs comme une petite valise avec des affaires personnelles (des variables).
Un closure est une fonction accompagnée de l'environnement (scope) qui existait au moment de sa création.
Exemple le plus simple de closure
Voyons ça en pratique :
Func<int> MakeCounter()
{
int count = 0;
return () =>
{
count++;
return count;
};
}
On l'appelle ainsi :
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
Comment ça marche ?
- La variable count est déclarée à l'intérieur de la méthode MakeCounter.
- La lambda () => { ... } est retournée vers l'extérieur et vit maintenant en dehors de la méthode.
- Mais ! Elle se souvient de la variable count, même si la méthode MakeCounter est déjà terminée.
C'est ça un closure : la lambda a « fermé » (capturé) la variable count depuis le contexte environnant.
Qu'est-ce que la lambda « capture » exactement ?
- les variables locales du method environnant (scope),
- les paramètres de la méthode,
- les variables dans les blocs (for, foreach, etc.).
Important : les variables sont capturées non pas par valeur, mais par référence ! Si on modifie la variable dans le closure, elle changera aussi «à l'extérieur». En réalité le compilateur C# crée une classe auxiliaire spéciale pour ces variables — mais il suffit de se souvenir de ça conceptuellement pour utiliser les closures en confiance.
2. Closure et portée lexicale
Affinons l'exemple de la « factory de fonctions » avec un closure :
Func<int, int> PowerFactory(int power)
{
return x =>
{
int result = 1;
for (int i = 0; i < power; i++)
result *= x;
return result;
};
}
Utilisation :
var square = PowerFactory(2); // x^2
var cube = PowerFactory(3); // x^3
Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2)); // 8
Les fonctions square et cube sont créées avec des valeurs différentes de la variable power, et chacune se souvient de la sienne. Pour chaque appel de PowerFactory un «sac à dos» propre des valeurs capturées est créé.
3. Mutation des variables capturées
Parfois on se demande : que se passe-t-il si dans une boucle on crée plusieurs lambdas qui capturent la variable de la boucle ? C'est là qu'on marche facilement sur une peau de banane.
Exemple : closure dans une boucle
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
action(); // ???
Qu'attendez-vous ? 0, 1, 2 ? En fait ce sera :
3
3
3
Pourquoi ? Toutes les lambdas «réfèrent» à la même variable i. À la fin de la boucle i vaut déjà 3, et c'est cette valeur que verront toutes nos Action.
Version corrigée
Pour que chaque lambda capture sa «propre» valeur, on crée une nouvelle variable à l'intérieur du corps de la boucle :
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
action(); // 0 1 2
Maintenant copy est une nouvelle variable à chaque itération, et le closure capture exactement celle-ci.
4. Utilisation des closures dans des tâches réelles
Traitement de données et callbacks
Quand vous faites quelque chose d'asynchrone ou que vous différerez l'exécution (handlers d'événements, filtrage, scheduling), un closure permet de «packager» la logique avec ses paramètres. Par exemple :
void ProcessList(List<int> list, int threshold)
{
var filtered = list.Where(x => x > threshold);
foreach (var item in filtered)
Console.WriteLine(item);
}
Ici la lambda dans Where capture la variable threshold.
Création de «factories» de fonctions
On passe un paramètre — on obtient une fonction qui a ce paramètre «intégré». Cette technique est pratique pour configurer des filtres, des comparators de tri, des réactions UI, etc.
Gestion d'état
Parfois il est utile de garder un peu d'état sans créer une classe séparée :
Func<string, string> CreateGreeting()
{
string prefix = "Hello";
return name =>
{
return $"{prefix}, {name}!";
};
}
5. Petits détails utiles
Sous le capot : comment fonctionne un closure en C#
Tout ce qui est capturé par une lambda, le compilateur le transforme en une classe auxiliaire : les variables deviennent des champs, et la lambda devient une méthode. Par conséquent l'état des variables «vit» entre les appels.
Chaque «factory» engendre un petit objet. C'est normal — .NET gère efficacement ces objets et les alloue seulement quand c'est vraiment nécessaire.
Rappel : n'capturez pas des objets «lourds» sans besoin
Si vous capturez un gros objet (par exemple une form UI), il ne sera pas libéré tant que la lambda est vivante. La raison classique d'une fuite est l'abonnement à des événements via une lambda qui a capturé un contexte «lourd» et l'absence de désabonnement (+=/-=).
Closures et durée de vie des collections — exemple avec LINQ
Les closures rendent LINQ flexible : les filtres se souviennent de leurs paramètres.
List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
Console.WriteLine(color); // "Blue"
Si on change ensuite startsWith, le résultat change aussi :
startsWith = "R";
foreach (var color in filtered)
Console.WriteLine(color); // "Red"
C'est parce que le closure référence la même variable startsWith, et la méthode StartsWith vérifie à chaque fois la valeur courante.
6. Erreurs typiques en travaillant avec les closures
Erreur n°1 : capturer une variable qui change avant son utilisation.
Situation classique dans les boucles : la lambda à l'intérieur du closure «regarde» la même variable de la boucle, qui au moment de l'appel a déjà une autre valeur. Au final la fonction travaille avec des données inattendues. Se corrige en introduisant une variable distincte à l'intérieur de la boucle.
Erreur n°2 : capturer un contexte trop large.
Le closure emporte avec lui tout l'objet au lieu d'un champ/valeur précis. Ça crée des dépendances inutiles et complique le code. Capturez seulement ce dont vous avez besoin.
Erreur n°3 : retenir des ressources lourdes et fuite de mémoire.
S'abonner à un événement avec une lambda qui a capturé un objet «lourd», et ne pas se désabonner — l'objet ne sera pas collecté. Surveillez la durée de vie des abonnements et utilisez le désabonnement explicite (-=).
Erreur n°4 : perte de lisibilité du code.
Un usage excessif des closures rend difficile de comprendre l'origine des données et leurs modifications, surtout si le closure est déclaré loin du point d'appel. Gardez la logique proche et n'en abusez pas.
GO TO FULL VERSION