Menu

Struct en C# : types valeur, struct ou classe et quand en utiliser une

Comment fonctionnent les structs en C# : la sémantique de valeur et la copie à l'affectation, l'erreur de modification dans une List<T>, les règles des constructeurs, l'égalité, le boxing, readonly struct et record struct, et une comparaison struct ou classe avec des conseils sur le moment où une struct est le bon choix.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Une struct est un type valeur que vous définissez vous-même. Elle ressemble à une classe, avec des champs, des propriétés, des méthodes et des constructeurs, mais une variable d'un type struct contient directement les données au lieu d'une référence vers un objet. Cette seule différence change le comportement de l'affectation, des appels de méthode, de l'égalité et de null.

Sémantique de valeur : l'affectation copie

Les types numériques intégrés, bool, char, DateTime, TimeSpan et Guid sont tous des structs. Les vôtres fonctionnent de la même façon :

Sortie :

struct: s1.X = 1, s2.X = 50
class:  c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150

Avec la struct, s2 est une copie indépendante, et Move travaille aussi sur sa propre copie. Pour permettre à une méthode de modifier la struct de l'appelant, passez-la par référence avec ref (voir ref et out). Pour éviter de copier une grande struct sans autoriser les modifications, C# 7.2 a ajouté les paramètres in.

Autres conséquences du fait d'être un type valeur :

  • Une variable struct ne peut jamais valoir null. PointS p = null; ne compile pas. Utilisez PointS? (un type valeur nullable) quand « pas de valeur » a un sens.
  • Une struct non initialisée est entièrement à zéro : champs numériques à 0, champs bool à false, champs référence à null.
  • Une struct ne peut pas hériter d'une autre struct ou classe, et rien ne peut hériter d'une struct. Elle peut implémenter des interfaces.
  • Les variables struct locales vivent en général sur la pile ou dans l'objet qui les contient, donc en créer une n'alloue rien sur le tas.

Le piège de la List de structs : CS1612

Comme lire une struct produit une copie, cette ligne d'apparence innocente ne compile pas :

var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable

L'indexeur de List<T> est une méthode qui renvoie une copie de l'élément. Modifier X sur cette copie temporaire ne changerait rien, donc le compilateur vous arrête. La même erreur apparaît quand une propriété renvoie une struct : order.Location.X = 10. Copiez, modifiez, réécrivez :

Sortie :

35
7

Les tableaux sont l'exception : array[0] est l'élément lui-même, pas une copie. Cette difficulté récurrente est la raison du conseil habituel de rendre les structs immuables : si une struct ne peut pas être modifiée, on ne perd rien à modifier une copie.

Constructeurs et valeurs par défaut

De C# 7 à C# 9, les règles des constructeurs de struct sont strictes :

  • Vous ne pouvez pas déclarer de constructeur sans paramètre. new PointS() existe toujours et met chaque champ à zéro.
  • Un constructeur que vous déclarez doit affecter chaque champ (et chaque propriété automatique) avant de se terminer.
  • Les initialiseurs de champs (public int X = 1;) ne sont pas autorisés sur les champs d'instance.

Sortie :

19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)

Money montre aussi pourquoi la valeur par défaut compte : un Money mis à zéro a une devise null, et votre code doit le gérer, car les tableaux, default(T) et les champs non initialisés en produisent tous.

Les versions plus récentes ont assoupli ces règles :

// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
    public int Retries = 3;
    public Settings() { }
}

// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.

Un constructeur sans paramètre de C# 10 s'exécute pour new Settings() mais pas pour default(Settings) ni pour les éléments de tableau, qui sont toujours entièrement à zéro. Cette différence surprend, donc utilisez-le avec prudence.

readonly struct (C# 7.2)

Marquer la struct elle-même readonly fait respecter l'immuabilité par le compilateur : chaque champ doit être readonly et chaque propriété automatique en lecture seule.

public readonly struct Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius) { Celsius = celsius; }

    public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta);   // returns a new value
}

En plus de documenter l'intention, cela aide les performances : quand une struct non readonly est stockée dans un champ readonly ou passée en paramètre in, le compilateur la copie avant chaque appel de méthode (il ne peut pas savoir que la méthode ne la modifiera pas). Une readonly struct n'a pas besoin de ces copies défensives. En C# 7.0, vous pouvez quand même rendre chaque champ readonly, comme le fait Money avec des propriétés en lecture seule : cela vous donne l'immuabilité, mais pas les copies économisées, car le compilateur ne fait confiance qu'à une struct déclarée readonly.

L'égalité

Equals sur une struct compare champ par champ par défaut, ce qui est le comportement de valeur voulu. Mais l'implémentation par défaut (ValueType.Equals) peut utiliser la réflexion et elle est lente, et l'opérateur == n'est pas défini du tout : a == b sur votre propre struct donne l'erreur CS0019. Implémentez les deux quand la struct sera comparée :

Sortie :

True
True
True

Implémenter IEquatable<T> compte pour les collections : HashSet<T>, Dictionary<TKey, TValue> et List<T>.Contains appellent directement Equals(GridCell) au lieu de boxer chaque valeur pour appeler Equals(object). Les record structs (voir plus bas) génèrent tout cela pour vous.

Le boxing

Convertir une struct en object ou en type d'interface la boxe : le runtime copie la valeur dans un nouvel objet sur le tas. La boîte et l'original sont ensuite indépendants :

Sortie :

2
0

Le boxing coûte une allocation à chaque fois, c'est pourquoi les anciennes collections non génériques comme ArrayList étaient lentes avec les types valeur, et pourquoi les génériques les ont remplacées. Cela signifie aussi qu'une struct modifiable manipulée via une interface est modifiée dans sa boîte, pas dans l'original, une raison de plus de garder les structs immuables.

Struct ou classe

structclasse
Naturetype valeurtype référence
Affectation et paramètrescopient les donnéescopient la référence
Peut valoir nullnon (T? le peut)oui
Valeur par défauttous les champs à zéronull
Héritageaucun ; peut implémenter des interfacesune seule classe de base, interfaces
==non défini sauf si vous le surchargezégalité de référence sauf surcharge
Equals par défautcompare les champscompare les références
Allocationen ligne (pile ou objet contenant)tas, ramasse-miettes
Adaptée àpetites valeurs immuablesentités, état volumineux ou partagé

Quand utiliser une struct

Choisissez une struct quand toutes ces conditions sont réunies : le type est une seule valeur logique (une coordonnée, un montant, une couleur, une plage de dates), il est petit (la recommandation de Microsoft est d'environ 16 octets, soit à peu près quatre int), il est immuable, et vous n'avez pas besoin d'héritage. Les structs sont rentables quand vous en créez un très grand nombre, par exemple des millions de points dans un tableau, car elles évitent une allocation sur le tas et une entrée pour le ramasse-miettes par élément.

Choisissez une classe pour tout ce qui a une identité (un client, une commande), tout ce qui est volumineux, tout ce qui est modifié depuis plusieurs endroits, et chaque fois que vous hésitez. Une grande struct modifiable cumule le coût des copies et la confusion de la modification de copies.

record struct (C# 10)

C# 10 a ajouté record struct, qui génère en une ligne l'égalité par valeur, ==, ToString et la prise en charge de with pour une struct :

public readonly record struct Coordinate(double Lat, double Lng);

var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same);            // True
Console.WriteLine(home);                    // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };

Un record struct simple a des propriétés positionnelles modifiables ; readonly record struct les rend init-only, ce qui est en général ce que vous voulez. La page sur les records couvre la version classe et montre les membres qu'écrit le compilateur.

Erreurs courantes

  • Modifier une struct via une copie. list[0].X = 1 (CS1612), une variable de foreach (CS1654), le résultat d'un getter de propriété. Réécrivez la copie modifiée, ou rendez la struct immuable.
  • Des structs volumineuses. Chaque affectation et chaque appel copie tout. Au-delà de quelques champs, une classe est en général plus rapide.
  • Oublier la valeur par défaut à zéro. Les tableaux et default(T) créent des valeurs struct sans exécuter votre constructeur, donc les champs que vous y validez peuvent encore valoir zéro ou null.
  • Comparer avec == sans l'avoir défini. CS0019. Implémentez IEquatable<T> et les opérateurs, ou utilisez un record struct.
  • Des structs modifiables derrière des interfaces. Le boxing fait que la modification porte sur une copie que vous ne regardez pas.

Questions fréquentes

Qu'est-ce qu'une struct en C# ?

Une struct est un type valeur défini par l'utilisateur : struct Point { public int X; public int Y; }. Une variable d'un type struct contient les données elles-mêmes plutôt qu'une référence vers un objet, donc l'affectation et le passage à une méthode copient toute la valeur. int, double, DateTime et Guid sont tous des structs.

Quelle est la différence entre une struct et une classe en C# ?

Une classe est un type référence : les variables partagent un même objet, et null est autorisé. Une struct est un type valeur : chaque variable a sa propre copie, elle ne peut pas valoir null (sauf avec Point?), elle ne peut ni hériter ni être héritée, et sa valeur par défaut a tous ses champs à zéro. Les structs conviennent aux petites valeurs immuables ; les classes aux entités qui ont une identité et un comportement.

Quand utiliser une struct plutôt qu'une classe en C# ?

Quand le type représente une seule petite valeur (une coordonnée, un montant, une plage de dates), idéalement immuable, comparée par son contenu, et créée en grand nombre là où éviter les allocations sur le tas compte. Les recommandations de Microsoft ajoutent une taille d'environ 16 octets ou moins. Si le type a une identité, de nombreux champs, ou est modifié via des références, utilisez une classe.

Pourquoi obtient-on « Cannot modify the return value because it is not a variable » ?

C'est l'erreur CS1612, typiquement due à list[0].X = 5 sur une List<Point> de structs. L'indexeur renvoie une copie de la struct, donc modifier la copie serait perdu, et le compilateur refuse. Copiez l'élément dans une variable, modifiez-la et réaffectez-la : var p = list[0]; p.X = 5; list[0] = p;. Les tableaux n'ont pas ce problème, car array[0] désigne l'élément lui-même.

Qu'est-ce qu'une readonly struct en C# ?

readonly struct (C# 7.2) déclare qu'aucun membre de la struct ne modifie son état : tous les champs doivent être readonly et les propriétés automatiques en lecture seule. Le compilateur le fait respecter, et cela lui permet d'éviter des copies défensives quand la struct est passée avec in ou stockée dans un champ readonly. La plupart des structs devraient être readonly.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER