Les Closures C# avant le framework .Net 4.5 Partie1 (Un comportement plutôt curieux)
Définition d’une fermeture (closure en anglais) selon Wikipedia
En informatique, une fermeture est une fonction de première classe avec des variables libres liées dans l’environnement lexical
Une fonction de première classe… ? Variables libres liéés dans l’environnement lexical…??
Une fonction de première classe signifie simplement qu’il s’agit d’une fonction concideré par C# (ou autre langage) en tant que type de donnée de première classe…
En termes plus simples, cela signifie qu’on peu assigner une fonction à une variable.
Variable libre
C’est une variable référencée dans une fonction. En revanche, cette variable n’est pas un paramètre ni une variable locale à la fonction. Elle y est juste référencée et utilisée.
Exemples
Dans ReturnFunc() y est référencée la variable ‘libre’ localvariable.
localvariable n’est pas un paramètre ni une variable locale au delegate(fonction anonyme) qui l’utilise.
Son rôle dans le delegate : s’incrémenter et se multiplier avec une variable locale au delegate.
private static Func<int, int> ReturnFunc() { var localvariable = 1; //'Free' localvariable is used inside of the delegate Func<int, int> myfunc = delegate(int var1) { Console.WriteLine("Value of localvariable: "+ localvariable); localvariable += 1; return var1 * localvariable; }; //When we return the myfunc, localvariable is bound inside of the delegate return myfunc; }
Nous faisons 2 appels successifs à ReturnFunc().
La sortie devrait être : 4 [(1+1)*2] puis 8 [(1+1)*4]
static void Main(string[] args) { //First class function assigned to variable v var v = ReturnFunc(); Console.WriteLine("First call"); Console.WriteLine("Result: "+v(2)); Console.WriteLine("Second call"); Console.WriteLine("Result: "+v(4)); Console.Read(); }
Sortie
First call Value of localvariable: 1 Result: 4 Second call Value of localvariable: 2 Result: 12
A chaque appel, on s’aperçoit que localvariable conserve sa valeure précédente avant de s’incrémenter !
Comportement étrange
Les variables locales ne sont-elles pas créées sur la pile? Ne partent-elles pas lorsque nous avons fini d’exécuter la méthode?
En d’autres termes, localvariable devrait être reinitialisée à chaque appel à ReturnFunc(), et donc valoir 1 !
Deduction
La variable libre localvariable vit maintenant avec la méthode renvoyé par ReturnFunc() : localvariable y est liée en dehors de sa portée d’origine.
Voilà ce qu’est une closure ! la variable libre localvariable est liée dans l’environnement lexical!
Un autre exemple
On créer une liste de fonction anonyme (‘Action’)
On utilise une boucle qui contient une variable libre ‘i’.
A chaque tour de boucle, on ajoute à la liste une fonction anonyme qui utilise la variable libre ‘i’
static void Main(string[] args) { var listAction = new List<Action>; for(int i=0; i<10; i++) { //'Free' i, is used inside of the delegate listAction.Add(() => Console.WriteLine(i)); } foreach(Action item in listAction) { //When we return the item, i is bound inside of the delegate item(); } Console.Read(); }
Sortie
Si nous n’étions pas avisé au sujet des closures, la sortie attendu serait 0 1 2 3 4 5 6 7 8 9
Mais nous savons que la variable libre ‘i’ est liée dans l’environnement lexical de la fonction anonyme ()=>console.WriteLine(i)
Nous observons le même phénomène.
10 10 10 10 10 10 10 10 10 10
En d’autres termes :
- Dans la première boucle, la variable libre ‘i’ s’itère de 0 à 9,
- La variable libre ‘i’ est l’utilisé dans l’expression lambda (la fonction anonyme). Chaque lambda obtient son accès à ‘i’ via la closure.
- A la fin de cette première boucle, ‘i’ = 10 (10 car on utilise l’opérateur post-incrémentation: i++ : lors de la dernière itération de la boucle, on augmente le compteur à 10 et on vérifie si ‘i'<10 ce qui n’est plus le cas, donc sortie de boulce)
- Ensuite, les ‘Action’ (ou fonctions anonymes ()=>Console.WriteLine(i)) s’executent : *elles ont toutes une closure sur la même variable libre ‘i’ qui vaut 10.
- Chaque ‘Action’ (fonction anonymes) affiche donc sa valeur actuelle : 10 sur la console.
*Point important pour comprendre la solution de contournement évoquée en partie 2