Frédéric Mélantois
C# 3.0 Beta, déclarations et initialisations simplifiées, regardons sous le capot !
Puisque le produit est encore en beta, portons un regard sur quelques nouveautés du langage et ce qui peut être généré par le compilateur c#.
Par Frédéric Mélantois publié le 25/06/2007 à 00:52, lu 4727 fois, 8 pages
 4 | Initialiseur de propriétés d'objets
L'initialisation des propriétés d'objets a été aussi simplifiée. On peut désormais instancier un objet et initialiser une partie ou la totalité de ses propriétés en une seule ligne :

public class MaClasse

{

    public MaClasse()

    {

    }

 

    public int MyProperty { get; set; }

}

 

static void Main(string[] args)

{

    MaClasse a = new MaClasse { MyProperty = 5 };

    // ou bien

    // var a = new MaClasse { MyProperty = 5 };

 

    Console.WriteLine(a.MyProperty.ToString());

    Console.ReadLine();

}

Cette façon d'écrire est bien plus confortable. Toutefois, prenons le temps de regarder le code IL en désassemblant via l'utilitaire ILDASM afin de le comparer avec le code que l'on on aurait pu écrire de manière plus classique :

public class MaClasse

{

    public MaClasse()

    {

    }

 

    public int MyProperty { get; set; }

}

 

static void Main(string[] args)

{

    MaClasse a = new MaClasse()

    a.MyProperty = 5;

 

    Console.WriteLine(a.MyProperty.ToString());

    Console.ReadLine();

}

Le lecteur ne notera pas de différences sur le résultat bien évidemment. Par contre, l'analyse du code IL produit dans la méthode statique « Main » montre des différences. Observons le code IL correspondant à l'instanciation d'objet et initialisation de propriétés en une seule ligne C# 3.0 :

.method private hidebysig static void  Main(string[] args) cil managed

{

  .entrypoint

  .maxstack  2

  .locals init ([0] class ConsoleApplication1.Program/MaClasse a, //variable locale que nous avons définie

           [1] class ConsoleApplication1.Program/MaClasse '<>g__initLocal1',//variable locale créée par le compilateur c#

           [2] int32 CS$0$0000)

  IL_0001:  newobj    instance void ConsoleApplication1.Program/MaClasse::.ctor() //Instanciation de MaClasse

  IL_0006:  stloc.1    //La référence est déchargée de la pile d'évaluation

  //pour être affecté à la variable locale '<>g__initLocal1'

  IL_0007:  ldloc.1    //Cette référence est remise sur la pile

  IL_0008:  ldc.i4.5    //La valeur 5 est placée sur la pile

  IL_0009:  callvirt   instance void ConsoleApplication1.Program/MaClasse::set_MyProperty(int32)//Déchargement de la pile

  //et affectation de la propriété.

  IL_000f:  ldloc.1    //La référence de '<>g__initLocal1' est mise sur la pile

  IL_0010:  stloc.0    // la variable locale 'a' prend la référence

  IL_0011:  ldloc.0    // La référence de 'a' est mise sur la pile

  IL_0012:  callvirt   instance int32 ConsoleApplication1.Program/MaClasse::get_MyProperty()

  IL_0017:  stloc.2    //Récupération de la valeur de la propriété et affectation à la variable locale CS$0$0000

  IL_0018:  ldloca.s   CS$0$0000 //mise sur la pile de la référence de cette variable.

  IL_001a:  call       instance string [mscorlib]System.Int32::ToString()

  IL_001f:  call       void [mscorlib]System.Console::WriteLine(string)

  IL_0025:  call       string [mscorlib]System.Console::ReadLine()

  IL_002a:  pop

  IL_002b:  ret

}

Il apparaît très curieux et surtout inutile de passer par une variable locale intermédiaire « '<>g__initLocal1' ». En effet, la mémoire réservée aux variables locales dans la méthode se trouve augmentée. Une instruction de chargement d'une référence dans la pile puis déchargement de cette dernière à destination de la variable locale que nous avions défini apparaît être une série d'instructions inutiles. Regardons, afin de comparer, le code IL correspondant au code c# plus classique ci-dessus :

.method private hidebysig static void  Main(string[] args) cil managed

  {

    .entrypoint

    .maxstack  2

    .locals init ([0] class ConsoleApplication1.Program/MaClasse a,//variable locale que nous avons définie

            [1] int32 CS$0$0000)

    IL_0001:  newobj    instance void ConsoleApplication1.Program/MaClasse::.ctor()//Instanciation de MaClasse

    IL_0006:  stloc.0    //La référence est déchargée de la pile d'évaluation

    //pour être affecté à la variable locale 'a'

    IL_0007:  ldloc.0    //Cette référence est remise sur la pile

    IL_0008:  ldc.i4.5    //La valeur 5 est placée sur la pile

    IL_0009:  callvirt   instance void ConsoleApplication1.Program/MaClasse::set_MyProperty(int32)//Déchargement de la pile

    //et affectation de la propriété.

    IL_000f:  ldloc.0    // La référence de 'a' est mise sur la pile

    IL_0010:  callvirt   instance int32 ConsoleApplication1.Program/MaClasse::get_MyProperty()

    IL_0015:  stloc.1    //Récupération de la valeur de la propriété et affectation à la variable locale CS$0$0000

    IL_0016:  ldloca.s   CS$0$0000

    IL_0018:  call       instance string [mscorlib]System.Int32::ToString()

    IL_001d:  call       void [mscorlib]System.Console::WriteLine(string)

    IL_0023:  call       string [mscorlib]System.Console::ReadLine()

    IL_0028:  pop

    IL_0029:  ret

  }

Dans les codes ci-dessus, la déclaration de variable locale et les instructions inutiles ont été mis en évidence en rouge.
L'utilitaire Reflector montre très bien la variable locale intermédiaire :

private static void Main(string[] args)

{

    MaClasse <>g__initLocal1 = new MaClasse();

    <>g__initLocal1.MyProperty = 5;

    MaClasse a = <>g__initLocal1;

    Console.WriteLine(a.MyProperty.ToString());

    Console.ReadLine();

}

On peut évidemment se demander, comme on me l'a fait remarquer à l'évocation de ces résultats, si le compilateur JIT ne réalise pas une optimisation qui éviterait l'usage d'une variable locale intermédiaire. Le meilleur moyen de le savoir est de regarder le code natif via l'utilitaire CorDbg fourni par le SDK. Le code généré est identique à une variable locale supplémentaire près. Voici les instructions supplémentaires en rouge :

00000000  push        edi  

00000001  push        esi 

00000002  push        ebx  //place dans la pile ebx

00000003  sub        esp,8

00000006  xor        eax,eax

00000008  mov        dword ptr [esp+4],eax

0000000c  mov        dword ptr [esp],ecx

0000000f  cmp        dword ptr ds:[00918A50h],0

00000016  je          0000001D

00000018  call        76E1F369

0000001d  xor        ebx,ebx // mise à zero de ebx

0000001f  xor        edi,edi

00000021  mov        ecx,919770h

00000026  call        FD680E04

0000002b  mov        esi,eax

0000002d  mov        ecx,esi

0000002f  call        dword ptr ds:[009197A8h]

00000035  mov        edi,esi

00000037  mov        ecx,edi

00000039  mov        edx,5

0000003e  cmp        dword ptr [ecx],ecx

00000040  call        dword ptr ds:[009197B0h]

00000046  mov        ebx,edi // inutile

00000048  mov        ecx,ebx // puisque que mov ecx,edi suffisait.

0000004a  cmp        dword ptr [ecx],ecx

0000004c  call        dword ptr ds:[009197ACh]

00000052  mov        esi,eax

00000054  mov        dword ptr [esp+4],esi

00000058  lea        ecx,[esp+4]

0000005c  call        760C15A0

00000061  mov        esi,eax

00000063  mov        ecx,esi

00000065  call        76138890

0000006a  call        7613851C           

00000071  add        esp,8

00000074  pop        ebx  // dépile ebx

00000075  pop        esi 

00000076  pop        edi 

00000077  ret 

Le compilateur JIT ne semble pas réaliser l'optimisation que l'on aurait souhaitée. Je déconseillerai donc d'utiliser la nouvelle syntaxe c# pour l'initialisation des propriétés ou champs accessibles d'une classe dans le cas où votre méthode demande une performance extrême. En effet, l'appel répété d'une méthode utilisant ce procédé, multiplierait d'autant les instructions inutiles vues plus haut. Pour une utilisation de tous les jours, on pourra accepter cette perte de performance car le code source C# se trouve simplifié.

public class MaClasse

{

    public MaClasse(int myProperty)

    {

        MyProperty = myProperty;

    }

 

    public int MyProperty { get; set; }

}

 

 

static void Main(string[] args)

{

    var a = new MaClasse(5) { MyProperty = 12 };

 

    Console.WriteLine(a.MyProperty.ToString());

    Console.ReadLine();

}

Le code ci-dessus ne doit vous laisser aucun doute sur la valeur retournée sur la console.
Une question que l'on peut se poser est l'imbrication des initialisations d'objets. Est-elle prise en charge ? L'exemple suivant montre qu'en effet non seulement cette imbrication est possible mais elle est correctement prise en charge au niveau de l'« Intellisence » de Visual Studio.

public class MaClasseA<T> where T : struct

{

    public MaClasseA()

    {

    }

 

    public T MyProperty1 { get; set; }

    public string MyProperty2 { get; set; }

}

 

public class MaClasseB<T> where T : struct

{

    public MaClasseB()

    {

    }

 

    public MaClasseA<T> MyProperty1 { get; set; }

    public float MyProperty2 { get; set; }

}

 

static void Main(string[] args)

{

    var a = new MaClasseB<int> { MyProperty1 = new MaClasseA<int>

            { MyProperty1 = (int)12.3, MyProperty2 = "Ma Propriété" } };

    Console.WriteLine(a.MyProperty1.MyProperty1.ToString());

    Console.ReadLine();

}

J'en profite pour souligner qu'il est possible d'initialiser plusieurs propriétés, en les séparant par des virgules. Il est aussi possible de ne pas initialiser les propriétés de son choix.
Vous remarquerez que dans la version Beta 1 d'ORCAS, l'« Intellisence » propose aussi les propriétés qui sont uniquement en lecture (en « get »), ce qui provoque une erreur à la compilation. Ce problème devrait être corrigé dans les prochaines builds d'ORCAS.
 
» Démarrer une discussion
 
Discussion démarée par Malkuth le 24/07/2007 à 03:11, 2 commentaire(s).
Discussion démarée par vjacquet le 25/06/2007 à 17:42, 4 commentaire(s).