C# struct (« types valeurs »)

Quelques raccourcis :

C# trace une distinction entre les struct, qu'il nomme aussi « types valeurs », et les class. Bien que le support offert aux class dans ce langage soit supérieur à celui offert aux struct, les deux familles de types ont leur rôle à jouer.

Cet article survole quelques caractéristiques clés des struct en C#.

Introduction

C# supporte deux grandes familles de types : les types « valeurs » et les types « références ». Techniquement, un type valeur est un struct, et un type référence est un class.

La relation entre ces deux types est très différente de celle qu’on rencontre en C++, où struct et class sont presque interchangeables. En , par exemple, int est un alias pour System.Int32, qui est un struct.

Notez que plusieurs diront que « les instances de types références sont allouées sur le tas » et que « les instances de types valeurs sont allouées sur la pile ». En pratique, ceci tend à s’avérer, mais nous ne pouvons compter là-dessus de manière portable (ce n’est pas documenté ainsi).

Quand représenter un type par un struct

Puisque les struct sont souvent placés sur la pile ou insérés à même la définition d’une classe, la recommandation officielle à leur égard est de restreindre les struct aux types qui sont à la fois :

Gestion de mémoire

Les struct, contrairement aux class, sont manipulés directement. Cela peut jouer un rôle dans un schème de saine gestion de la mémoire.

Par exemple, dans l'exemple à droite, où le type Point est un class, le tableau pts est fait de références contiguës en mémoire, mais les référés de ces références sont épars. Ceci impacte à la fois l'espace occupé par le tableau et la vitesse d'accès aux éléments (car les accès indirects aux objets sont moins Cache-Friendly que les accès directs)

var pts = new []
{
   new Point(), new Point(2,3), new Point() { X = -1, Y = -1 }
};

class Point
{
   public int X { get; init; } = 0;
   public int Y { get; init; } = 0;
   public Point() {}
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}

Il serait tentant de simplement remplacer class par struct et d'espérer pour le mieux, mais cela ne fonctionnera pas car les règles (pour des raisons parfois obscures) pour l'initialisation d'un struct sont différentes de celles qui prévalent pour l'initialisation d'un class.

Par exemple, initialiser une propriété à la déclaration n'est pas possible pour un struct.

On peut spéculer que l'idée est qu'instancier un struct par défaut réalise un memset() sur l'espace mémoire de cet objet, pour des raisons d'efficacité, mais c'est de la spéculation

var pts = new []
{
   new Point(), new Point(2,3), new Point() { X = -1, Y = -1 }
};

// ceci ne compile pas
struct Point
{
   public int X { get; init; } = 0 ;// Non
   public int Y { get; init; } = 0 ;// Non
   public Point() {} // Ok, initialise tout à zéro
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}

L'écriture correcte est celle proposée à droite :

  • Pas d'initialisation par défaut des propriétés (l'initialisation à zéro est automatique)
  • Pas de constructeur par défaut explicite (c'est l'initialisation à zéro de toutes les propriétés et de tous les attributs qui prévaut)
  • Avec un tableau d'éléments de type struct, les valeurs du  tableau sont contiguës en mémoire
  • Notez qu'il est possible d'appeler le constructeur par défaut d'un struct; ce qui est illégal, c'est de définir soi-même le comportement de ce constructeur
var pts = new [] // Ok; valeurs contiguës
{
   new Point(), // Ok, implicitement 0 pour tous les états
   new Point(2,3), // Ok
   new Point() { X = -1, Y = -1 } // Ok
};

struct Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public Point()
   {
   }
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}

Alias ou copies

La principale caractéristique des struct est que les objets de cette famille de types sont manipulés directement, alors que les objets dont les types sont de la famille des class sont manipulés indirectement. Ceci a pour conséquence que copier un struct copie bel et bien le struct, alors que copier une référence sur un class crée un alias, une situation où deux références partagent un même référé.

 

À titre d'exemple, si nous supposons un Point représenté sous forme d'un class, alors dans le programme principal visible à droite, l'expression suivante :

Point pt1 = pt0;

... fait pointer pt1 au même endroit que pt0. Aucun Point n'est copié, malgré les apparences; nous faisons ainsi une copie de référence, pas une copie de référé.

Conséquemment, l'expression suivante :

pt1.X++;

... modifie le référé de pt1, qui est aussi le référé de pt0. Ainsi, pt0.X et pt1.X sont une seule et même entité, et les deux affichages montrent 1 à l'écran.

using System;

Point pt0 = new(); // 0, 0
Point pt1 = pt0; // alias
pt1.X++;
Console.WriteLine(pt0.X); // 1
Console.WriteLine(pt1.X); // 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;
   }
}

 

Maintenant, si nous supposons un Point représenté sous forme d'un struct, alors dans le programme principal visible à droite, l'expression suivante :

Point pt1 = pt0;

... fait de pt1 une copie de pt0. Il y a alors bel et bien copie d'un Point, par copie d'une référence sur un Point.

Conséquemment, l'expression suivante :

pt1.X++;

... modifie pt1, qui est un objet distinct de pt0. Ainsi, les deux affichages montrent respectivement 0 et 1 à l'écran.

using System;

Point pt0 = new (); // 0, 0
Point pt1 = pt0; // copie!
pt1.X++;
Console.WriteLine(pt0.X); // 0
Console.WriteLine(pt1.X); // 1

struct Point
{
   public int X { get; set; }
   public int Y { get; set; }
   public Point() {}
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !