C# – Initialisation

Quelques raccourcis :

Comme la plupart des langages OO, C# offre une multitude de mécanismes pour initialiser des objets ou des collections. Voici un bref survol de ce sujet toujours en mouvement.

Notez que les règles de C# en ce qui a trait à l'initialisation ne sont pas homogènes; il est utile de comprendre les différences et les nuances entre les diverses facettes de cette important aspect de la programmation pour tirer profit de ce langage.

Initialiser des instances de « types valeurs »

Un volet de C# pour lequel l'initialisation implique des opérations un peu « magiques » est l'initialisation des « types valeurs » (les struct dans ce langage).

Initialisation directe

Bien que les « primitifs » de C# comme int, float ou bool soient des « types valeurs », et soient en fait des alias pour des noms du système de types commun de la plateforme .NET (p.ex. : System.Int32 pour int, System.Single pour float, etc.), ces types n'ont pas droit au même traitement que les struct que vous pourriez écrire vous-mêmes.

Par exemple :

struct Entier
{
   public int Valeur { get; set; }
   public Entier(int valeur)
   {
      Valeur = valeur;
   }
}
public class Program
{
   static void Main()
   {
      int n0 = 3; // Ok, magique
      Int32 n1 = 3; // Ok, magique
      // int n3 = new int(3); // illégal
      // Int32 n3 = new Int32(3); // illégal
      Entier e0 = new Entier(3); // Ok
   }
}

On ne peut pas, à ma connaissance, écrire nous-mêmes un type pour lequel l'initialisation des instances est aussi simple que pour les « primitifs » de C# dans ce langage.

Initialisation automatique des propriétés autogénérées

C# ne permet pas d'exprimer une initialisation automatique des propriétés autogénérées pour un struct, celles-ci étant toujours initialisées au zéro du type (default). Par exemple :

using System;

struct Entier
{
   public int Valeur { get; set; } = 0; // illégal pour un struct
}

public class Program
{
   static void Main()
   {
      Entier e = new Entier(); // serait Ok si on enlève le "=0" de l'initialisation de Entier.Valeur
   }
}

Notez qu'en C#, le fait d'écrire new X(...) pour un type X donné, class ou struct, initialise la zone mémoire où ce X est placé avec des zéros avant de construire l'objet.

Les opérations d'initialisation faites avant (p.ex. : initialisation directe des propriétés et des attributs) et pendant la construction se font en plus de cette initialisation à zéro, ce qui rend chaque construction relativement coûteuse dans ce langage.

Constructeur par défaut

C# ne permet pas d'écrire un constructeur par défaut pour un struct. Ce constructeur existe implicitement et initialise les attributs et les propriétés d'instance à zéro (default).

Constructeur paramétrique

Il est possible d'écrire un constructeur paramétrique pour un struct en C#. Par exemple :

struct Entier
{
   public int Valeur { get; set; }
   public Entier(int valeur) // Ok
   {
      Valeur = valeur;
   }
}
public class Program
{
   static void Main()
   {
      Entier e0 = new Entier(3); // Ok
   }
}

Initialisation et états nommés

Si un struct a des états mutables (attributs publics ou propriétés dont le mutateur est public), il est possible d'initialiser ces états par leur nom même en l'absence d'un constructeur explicite. Par exemple :

using System;

struct Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
   public override string ToString() => $"({X},{Y})";
}

public class Program
{
   static void Main()
   {
      Point p0 = new Point { }; // 0,0
      Point p1 = new Point(); // 0,0
      Point p2 = new Point(2, 3); // 2,3 et passe par le constructeur paramétrique
      Point p3 = new Point { X = 2, Y = 3 }; // 2,3 et n'appelle pas le constructeur paramétrique
      Point p4 = new Point { Y = 3 }; // 0,3 et n'appelle pas le constructeur paramétrique
      Console.WriteLine($"{p0} {p1} {p2} {p3} {p4}");
   }
}

Ici, du fait que le type Point est un struct, il possède un constructeur par défaut implicite.

Il est possible de combiner divers mécanismes d'initialisation, par exemple :

using System;

struct Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public int Z { get; set; }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
      Z = 0; // requis pour compiler
   }
   public override string ToString() => $"({X},{Y},{Z})";
}

public class Program
{
   static void Main()
   {
      Point p0 = new Point { }; // 0,0,0
      Point p1 = new Point(); // 0,0,0
      Point p2 = new Point(2, 3); // 2,3,0 et passe par le constructeur paramétrique
      Point p3 = new Point { X = 2, Y = 3 }; // 2,3,0 et n'appelle pas le constructeur paramétrique
      Point p4 = new Point { Y = 3 }; // 0,3,0 et n'appelle pas le constructeur paramétrique
      Point p5 = new Point(2, 3){ Z = 1 }; // 2,3,1 et passe par le constructeur paramétrique
      Console.WriteLine($"{p0} {p1} {p2} {p3} {p4} {p5}");
   }
}
  Ceci... ... équivaut à cela

Cette forme est en fait un raccourci syntaxique :

struct Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}
class Program
{
   static void Main()
   {
      Point p = new Point { X = 1, Y = 1 };
   }
}
struct Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}
class Program
{
   static void Main()
   {
      Point p = new Point();
      p.X = 1;
      p.Y = 1;
   }
}

Le code généré est le même si vous le regardez avec le décompilateur ILDASM :

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       25 (0x19)
  .maxstack  2
  .locals init (valuetype Point V_0)
  IL_0000:  ldloca.s   V_0
  IL_0002:  initobj    Point
  IL_0008:  ldloca.s   V_0
  IL_000a:  ldc.i4.1
  IL_000b:  call       instance void Point::set_X(int32)
  IL_0010:  ldloca.s   V_0
  IL_0012:  ldc.i4.1
  IL_0013:  call       instance void Point::set_Y(int32)
  IL_0018:  ret
} // end of method Program::Main
.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       25 (0x19)
  .maxstack  2
  .locals init (valuetype Point V_0)
  IL_0000:  ldloca.s   V_0
  IL_0002:  initobj    Point
  IL_0008:  ldloca.s   V_0
  IL_000a:  ldc.i4.1
  IL_000b:  call       instance void Point::set_X(int32)
  IL_0010:  ldloca.s   V_0
  IL_0012:  ldc.i4.1
  IL_0013:  call       instance void Point::set_Y(int32)
  IL_0018:  ret
} // end of method Program::Main

Initialiser des instances de « types références »

L'initialisation des « types références » (des class en C#) ressemble à celle des types valeurs, à ceci-près qu'elle permet aussi d'écrire un constructeur par défaut et d'initialiser automatiquement les propriétés autogénérées.

Initialisation automatique des propriétés autogénérées

C# permet d'exprimer une initialisation automatique des propriétés autogénérées pour un class. Par exemple :

using System;

class EntierNaturel
{
   static int Valider(int valeur) => valeur > 0? valeur : throw new ArgumentException();
   public int Valeur { get; private set; } = 1;
   public EntierNaturel()
   {
   }
   public EntierNaturel(int valeur)
   {
      Valeur = Valider(valeur);
   }
   public override string ToString() => $"{Valeur}";
}

public class Program
{
   static void Main()
   {
      EntierNaturel e0 = new EntierNaturel();
      EntierNaturel e1 = new EntierNaturel(3);
      Console.WriteLine($"{e0} {e1}"); // 1 3
   }
}

Notez que si vous souhaitez utiliser une propriété qui ne soit pas autogénérée, par exemple pour loger la validation dans son mutateur, vous pourrez initialiser l'attribut sous-jacent mais pas la propriété elle-meme :

using System;

class EntierNaturel
{
   static int Valider(int valeur) => valeur > 0? valeur : throw new ArgumentException();
   private int valeur = 1; // Ok
   public int Valeur { get => valeur; private set { valeur = Valider(value); } /* = 1; */ // initialisation illégale ici
   public EntierNaturel()
   {
   }
   public EntierNaturel(int valeur)
   {
      Valeur = valeur;
   }
   public override string ToString() => $"{Valeur}";
}

public class Program
{
   static void Main()
   {
      EntierNaturel e0 = new EntierNaturel();
      EntierNaturel e1 = new EntierNaturel(3);
      Console.WriteLine($"{e0} {e1}"); // 1 3
   }
}

Constructeur par défaut

Comme le montre l'exemple de la classe EntierNaturel ci-dessus, il est possible d'écrire un constructeur par défaut pour un class.

Constructeur paramétrique

Comme le montre l'exemple de la classe EntierNaturel ci-dessus, il est possible d'écrire un constructeur paramétrique pour un class.

Initialisation et états nommés

Si un class a des états mutables (attributs publics ou propriétés dont le mutateur est public), il est possible d'initialiser ces états par leur nom même en l'absence d'un constructeur explicite. Notez toutefois que la construction par défaut demeure nécessaire si au moins un constructeur autre que celui-ci existe. Par exemple :

using System;

class Point
{
   public int X { get; set; } = 0;
   public int Y { get; set; } = 0;
   public Point() // nécessaire ici car pour un class, n'existe pas si on
   {              // écrit explicitement un autre constructeur
   }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
   public override string ToString() => $"({X},{Y})";
}

public class Program
{
   static void Main()
   {
      Point p0 = new Point { }; // 0,0
      Point p1 = new Point(); // 0,0
      Point p2 = new Point(2, 3); // 2,3 et passe par le constructeur paramétrique
      Point p3 = new Point { X = 2, Y = 3 }; // 2,3 et n'appelle pas le constructeur paramétrique
      Point p4 = new Point { Y = 3 }; // 0,3 et n'appelle pas le constructeur paramétrique
      Console.WriteLine($"{p0} {p1} {p2} {p3} {p4}");
   }
}

Il est possible de combiner divers mécanismes d'initialisation, par exemple :

using System;

class Point
{
   public int X { get; set; } = 0;
   public int Y { get; set; } = 0;
   public int Z { get; set; } = 0;
   public Point() // nécessaire pour l'exemple
   {
   }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   } // Z implicitement zéro
   public override string ToString() => $"({X},{Y},{Z})";
}

public class Program
{
   static void Main()
   {
      Point p0 = new Point { }; // 0,0,0
      Point p1 = new Point(); // 0,0,0
      Point p2 = new Point(2, 3); // 2,3,0 et passe par le constructeur paramétrique
      Point p3 = new Point { X = 2, Y = 3 }; // 2,3,0 et n'appelle pas le constructeur paramétrique
      Point p4 = new Point { Y = 3 }; // 0,3,0 et n'appelle pas le constructeur paramétrique
      Point p5 = new Point(2, 3){ Z = 1 }; // 2,3,1 et passe par le constructeur paramétrique
      Console.WriteLine($"{p0} {p1} {p2} {p3} {p4} {p5}");
   }
}

  Ceci... ... équivaut à cela

Cette forme est en fait un raccourci syntaxique :

class Point
{
   public int X { get; set; } = 0;
   public int Y { get; set; } = 0;
   public Point() { }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}
class Program
{
   static void Main()
   {
      Point p = new Point { X = 1, Y = 1 };
   }
}
class Point
{
   public int X { get; set; } = 0;
   public int Y { get; set; } = 0;
   public Point() { }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}
class Program
{
   static void Main()
   {
      Point p = new Point();
      p.X = 1;
      p.Y = 1;
   }
}

Le code généré est presque le même si vous le regardez avec le décompilateur ILDASM :

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       21 (0x15)
  .maxstack  8
  IL_0000:  newobj     instance void Point::.ctor()
  IL_0005:  dup
  IL_0006:  ldc.i4.1
  IL_0007:  callvirt   instance void Point::set_X(int32)
  IL_000c:  dup
  IL_000d:  ldc.i4.1
  IL_000e:  callvirt   instance void Point::set_Y(int32)
  IL_0013:  pop
  IL_0014:  ret
} // end of method Program::Main
.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       19 (0x13)
  .maxstack  8
  IL_0000:  newobj     instance void Point::.ctor()
  IL_0005:  dup
  IL_0006:  ldc.i4.1
  IL_0007:  callvirt   instance void Point::set_X(int32)
  IL_000c:  ldc.i4.1
  IL_000d:  callvirt   instance void Point::set_Y(int32)
  IL_0012:  ret
} // end of method Program::Main

Synthétiser des instances de types anonymes

C# permet de synthétiser des instances de types anonymes avec des propriétés nommées, ce qui est particulièrement utile pour des mécanismes comme LiNQ (il s'agit d'ailleurs bien de propriétés et non pas d'attributs; le code généré contient des set et des get). Par exemple 

using System;
public class Program
{
   static void Main()
   {
      var x = new { X = 3, Y = 4 };
      Console.WriteLine($"{x.X} {x.Y}"); // 3 4
   }
}

Initialisation et indexeurs

C# offre un support syntaxique spécial à l'initialisation pour les types munis d'indexeurs. Par exemple :

using System;
using System.Runtime.InteropServices.ComTypes;

class TicTacToe
{
   const int TAILLE = 3;
   public enum État { Vide, X, O }
   private État[,] Grille { get; } = new État[TAILLE, TAILLE]
   {
      { État.Vide, État.Vide, État.Vide },
      { État.Vide, État.Vide, État.Vide },
      { État.Vide, État.Vide, État.Vide }
   };
   public État this[int ligne, int colonne]
   {
      get => Grille[ligne, colonne];
      set
      {
         Grille[ligne, colonne] = value; // ajouter de la validation au goût
      }
   }
   public TicTacToe()
   {
   }
   // ...
   public override string ToString()
   {
      string res = "";
      for (int ligne = 0; ligne != TAILLE; ++ligne)
      {
         for (int colonne = 0; colonne != TAILLE; ++colonne)
            switch (Grille[ligne, colonne])
            {
               case État.O: res += " O "; break;
               case État.X: res += " X "; break;
               case État.Vide: res += "   "; break;
               default: throw new Exception();
            }
         res += '\n';
      }
      return res;
   }
}

public class Program
{
   static void Main()
   {
      var ttt = new TicTacToe()
      {
         [0, 0] = TicTacToe.État.O,
         [0, 1] = TicTacToe.État.X,
         [1, 0] = TicTacToe.État.O,
         [1, 2] = TicTacToe.État.X,
         [2, 0] = TicTacToe.État.X,
         [2, 1] = TicTacToe.État.X,
         [2, 2] = TicTacToe.État.O
      };
      Console.WriteLine(ttt);
   }
}
  Ceci... ... équivaut à cela

Encore une fois, il s'agit d'un raccourci syntaxique :

// ...
class Program
{
   static void Main()
   {
      var ttt = new TicTacToe()
      {
         [0, 0] = TicTacToe.État.O,
         [0, 1] = TicTacToe.État.X,
         [1, 0] = TicTacToe.État.O,
         [1, 2] = TicTacToe.État.X,
         [2, 0] = TicTacToe.État.X,
         [2, 1] = TicTacToe.État.X,
         [2, 2] = TicTacToe.État.O
      };
   }
}
// ...
class Program
{
   static void Main()
   {
      var ttt = new TicTacToe();
      ttt[0, 0] = TicTacToe.État.O;
      ttt[0, 1] = TicTacToe.État.X;
      ttt[1, 0] = TicTacToe.État.O;
      ttt[1, 2] = TicTacToe.État.X;
      ttt[2, 0] = TicTacToe.État.X;
      ttt[2, 1] = TicTacToe.État.X;
      ttt[2, 2] = TicTacToe.État.O;
   }
}

Le code généré est presque le même si vous le regardez avec le décompilateur ILDASM :

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       70 (0x46)
  .maxstack  5
  IL_0000:  newobj     instance void TicTacToe::.ctor()
  IL_0005:  dup
  IL_0006:  ldc.i4.0
  IL_0007:  ldc.i4.0
  IL_0008:  ldc.i4.2
  IL_0009:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_000e:  dup
  IL_000f:  ldc.i4.0
  IL_0010:  ldc.i4.1
  IL_0011:  ldc.i4.1
  IL_0012:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0017:  dup
  IL_0018:  ldc.i4.1
  IL_0019:  ldc.i4.0
  IL_001a:  ldc.i4.2
  IL_001b:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0020:  dup
  IL_0021:  ldc.i4.1
  IL_0022:  ldc.i4.2
  IL_0023:  ldc.i4.1
  IL_0024:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0029:  dup
  IL_002a:  ldc.i4.2
  IL_002b:  ldc.i4.0
  IL_002c:  ldc.i4.1
  IL_002d:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0032:  dup
  IL_0033:  ldc.i4.2
  IL_0034:  ldc.i4.1
  IL_0035:  ldc.i4.1
  IL_0036:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_003b:  dup
  IL_003c:  ldc.i4.2
  IL_003d:  ldc.i4.2
  IL_003e:  ldc.i4.2
  IL_003f:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0044:  pop
  IL_0045:  ret
} // end of method Program::Main
.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       68 (0x44)
  .maxstack  5
  IL_0000:  newobj     instance void TicTacToe::.ctor()
  IL_0005:  dup
  IL_0006:  ldc.i4.0
  IL_0007:  ldc.i4.0
  IL_0008:  ldc.i4.2
  IL_0009:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_000e:  dup
  IL_000f:  ldc.i4.0
  IL_0010:  ldc.i4.1
  IL_0011:  ldc.i4.1
  IL_0012:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0017:  dup
  IL_0018:  ldc.i4.1
  IL_0019:  ldc.i4.0
  IL_001a:  ldc.i4.2
  IL_001b:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0020:  dup
  IL_0021:  ldc.i4.1
  IL_0022:  ldc.i4.2
  IL_0023:  ldc.i4.1
  IL_0024:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0029:  dup
  IL_002a:  ldc.i4.2
  IL_002b:  ldc.i4.0
  IL_002c:  ldc.i4.1
  IL_002d:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0032:  dup
  IL_0033:  ldc.i4.2
  IL_0034:  ldc.i4.1
  IL_0035:  ldc.i4.1
  IL_0036:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_003b:  ldc.i4.2
  IL_003c:  ldc.i4.2
  IL_003d:  ldc.i4.2
  IL_003e:  callvirt   instance void TicTacToe::set_Item(int32,
                                                         int32,
                                                         valuetype TicTacToe/'État')
  IL_0043:  ret
} // end of method Program::Main

Notez que nous pourrions utiliser des valeurs de types variés pour les indices comme pour les valeurs à utiliser pour fins d'initialisation si les indexeurs appropriés étaient définis :

using System;
class Étrange
{
   public string this[int n] { get => ""; set { } }
   public double this[string s,char c] { get => 0.0; set { } }
}
public class Program
{
   static void Main()
   {
      var e = new Étrange
      {
         [3] = "bizarre",
         ["3",'3'] = 3.14159
      };
   }
}

Initialiser des collections

En C#, si vous exprimez une collection comme étant IEnumerable et comme exposant une méthode Add (par voie de méthode d'extension ou en tant que méthode d'instance) de signature appropriée, il sera aussi possible d'exprimer l'initialisation de votre collection de manière raccourcie.

Notez que dans le code ci-dessous, IEnumerable est implémentée... de manière absolument non-pertinente, mais l'initialisation fonctionne, ce qui nous apprend que la contrainte de C# est incohérente. Cela dit, faut faire avec, c'est C#... :

using System;
using System.Collections;
using System.Collections.Generic;

// note : terriblement inefficace
class Tapon<T> : IEnumerable<T>
{
   T[] Données { get; set; } = new T[0];
   public Tapon()
   {
   }
   public void Add(T elem)
   {
      var temp = new T[Données.Length + 1];
      Array.Copy(Données, temp, Données.Length);
      temp[Données.Length] = elem;
      Données = temp;
   }
   public override string ToString()
   {
      string res = "";
      if (Données.Length == 0) return res;
      res += Données[0];
      for (int i = 1; i < Données.Length; ++i)
         res += $" {Données[i]}";
      return res;
   }

   IEnumerator<T> IEnumerable<T>.GetEnumerator()
   {
      throw new NotImplementedException();
   }

   IEnumerator IEnumerable.GetEnumerator()
   {
      throw new NotImplementedException();
   }
}
public class Program
{
   static void Main()
   {
      var tapon = new Tapon<int>() { 2, 3, 5, 7, 11 };
      Console.WriteLine(tapon);
   }
}

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !