Les Closures C# avant le framework .Net 4.5 Partie3 (Comprendre ce qu’il se passe en coulisse)

Demystification

On sait que de manière générale les types valeurs sont stockées sur la pile, et que les types références sont sur le tas (le pointer sur ces références étant sur la pile)

A la sortie de la fonction ReturnFunc, les types valeurs (dont fait parti localvariable) sont dites ‘unwind’: elles disparaissent de la pile.

A la création de la fonction anonyme (delegate) qui fait une closure sur la variable ‘libre’ localvariable : le compilateur détecte la closure.

Non seulement il stock le code du delegate sur le tas mais il y alloue aussi un slot mémoire contenant localvariable!

D’où le fait que localvariable persiste !


Note

Le garbage collector sait gérer la mémoire allouée sur le tas mais pas sur la pile.

Sur le tas, tant qu’une fonction pointe ou utilise une variable local, celle-ci n’est pas garbage collecté.

Si on tente de créer une Struct, et d’y implémenter une closure sur un champs de cette Struct : le compilateur C# nous interdit cette opération.

En effet, la closure pointerai sur un champs spécifique d’une Struct (Struct étant sur la pile) qui est amené à être unwind…

En revanche, si on créé une class, et qu’on y implémente une closure sur une variable local : le compilateur autorise cette opération car la class est copié sur le tas ainsi que ses variables locales (le garbage collector sait gérer les alloations mémoires).

struct myStruct
{
    public int i;
    public Func<int> fct()
    {
        Func<int> f = () => i;//<-Not authorized !
        return f;
    }
}

Simulons le traitement du compilateur

Tous ce passe comme si une nouvelle class (qu’on appellera ReturnFuncClosure) était créée.

Cette class contenant une variable local localvariable, et une fonction qui prends en paramètre un entier, et retourne un entier.


class ReturnFuncClosure
{
    public int localvariable = 1;

    public int Fct(int var1)
    {
        Console.WriteLine("Value of localvariable: " + localvariable);
        localvariable += 1;
        return var1 * localvariable;
    }
}

On appelle ensuite les membres de cette classe dans notre closure “fait à la main”

private static Func<int, int> ReturnFuncMadeManually()
{
    /*
    Instanciate ReturnFuncClosure. 
    'closure' is on the Heap. 
    A pointer to closure is on the Stack.
    */
    var closure = new ReturnFuncClosure();

    /*
    localvariable belonging to object closure is persisted on the heap
    */
    closure.localvariable = 1;

    /*
    myfunc pointer is on the stack.
    myfunc pointer target method Fct belonging to closure object.
    Hence Fct is persisted on the heap.
    */
    Func<int, int>myfunc = new Func<int, int>(closure.Fct);
    
    return myfunc;
}

Appel

static void Main(string[] args)
{
    var v = ReturnFuncMadeManually();

    Console.WriteLine("First call");
    Console.WriteLine("Result: "+v(2));
    Console.WriteLine("Second call");
    Console.WriteLine("Result: "+v(4));
}

Sortie

First call
Value of localvariable: 1
Result: 4
Second call
Value of localvariable: 2
Result: 12

 

Leave a Reply

Your email address will not be published. Required fields are marked *