Types de structure (référence C#)

Un type de structure (ou type de struct) est un type valeur qui peut encapsuler des données et des fonctionnalités associées. Vous utilisez le mot clé struct pour définir un type de structure :

public struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; }
    public double Y { get; }

    public override string ToString() => $"({X}, {Y})";
}

Pour plus d’informations sur les types ref struct et readonly ref struct, consultez l’article sur les types de structure ref.

Les types de structure ont une sémantique de valeur. Autrement dit, une variable d’un type de structure contient une instance du type. Par défaut, les valeurs de variable sont copiées lors de l’affectation, en transformant un argument en une méthode et en retournant un résultat de méthode. Pour les variables de type structure, une instance du type est copiée. Pour plus d’informations, consultez Types valeur.

En règle générale, vous utilisez des types de structure pour concevoir de petits types centrés sur les données qui fournissent peu ou pas de comportement. Par exemple, .NET utilise des types de structure pour représenter un nombre (entier et réel), une valeur booléenne, un caractère Unicode, une instance temporelle. Si vous vous concentrez sur le comportement d’un type, envisagez de définir une classe. Les types de classe ont une sémantique de référence. Autrement dit, une variable d’un type de classe contient une référence à une instance du type, et non à l’instance elle-même.

Étant donné que les types de structure ont une sémantique de valeur, nous vous recommandons de définir des types de structure immuables.

readonly struct

Vous utilisez le modificateur readonly pour déclarer qu’un type de structure est immuable. Tous les membres de données d’une struct readonly doivent être en lecture seule comme suit :

Cela garantit qu’aucun membre d’une struct readonly modifie l’état de la struct. Cela signifie que d’autres membres d’instance, à l’exception des constructeurs, sont implicitement readonly.

Notes

Dans une struct readonly, un membre de données d’un type de référence mutable peut toujours muter son propre état. Par exemple, vous ne pouvez pas remplacer une instance List<T>, mais vous pouvez y ajouter de nouveaux éléments.

Le code suivant définit un struct readonly avec des setters de propriétés init uniquement :

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

    public override string ToString() => $"({X}, {Y})";
}

Membres d'instance readonly

Vous pouvez également utiliser le modificateur readonly pour déclarer qu’un membre d’instance ne modifie pas l’état d’une struct. Si vous ne pouvez pas déclarer le type de structure entier en tant que readonly, utilisez le readonly modificateur pour marquer les membres d’instance qui ne modifient pas l’état de la struct.

Dans un membre d’instance readonly, vous ne pouvez pas affecter les champs d’instance de la structure. Toutefois, un membre readonly peut appeler un membre non readonly. Dans ce cas, le compilateur crée une copie de l’instance de structure et appelle le membre non readonly sur cette copie. Par conséquent, l’instance de structure d’origine n’est pas modifiée.

En règle générale, vous appliquez le modificateur readonly aux types de membres d’instance suivants :

  • méthodes :

    public readonly double Sum()
    {
        return X + Y;
    }
    

    Vous pouvez également appliquer le modificateur readonly aux méthodes qui remplacent les méthodes déclarées dans System.Object :

    public readonly override string ToString() => $"({X}, {Y})";
    
  • propriétés et indexeurs :

    private int counter;
    public int Counter
    {
        readonly get => counter;
        set => counter = value;
    }
    

    Si vous devez appliquer le modificateur readonly aux deux accesseurs d’une propriété ou d’un indexeur, appliquez-le dans la déclaration de la propriété ou de l’indexeur.

    Remarque

    Le compilateur déclare un get accesseur d’une propriété readonlyimplémentée automatiquement, quelle que soit la readonly présence du modificateur dans une déclaration de propriété.

    Vous pouvez appliquer le modificateur readonly à une propriété ou un indexeur avec un accesseur init :

    public readonly double X { get; init; }
    

Vous pouvez appliquer le modificateur readonly aux champs statiques d’un type de structure, mais pas à d’autres membres statiques, tels que des propriétés ou des méthodes.

Le compilateur peut utiliser le modificateur readonly pour les optimisations des performances. Pour plus d’informations, consultez Éviter les allocations.

Mutation non destructrice

À compter de C# 10, vous pouvez utiliser l’expression with pour produire une copie d’une instance de type structure avec les propriétés et champs spécifiés modifiés. Vous utilisez la syntaxe d’initialiseur d’objet pour spécifier les membres à modifier et leurs nouvelles valeurs, comme l’illustre l’exemple suivant :

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

    public override string ToString() => $"({X}, {Y})";
}

public static void Main()
{
    var p1 = new Coords(0, 0);
    Console.WriteLine(p1);  // output: (0, 0)

    var p2 = p1 with { X = 3 };
    Console.WriteLine(p2);  // output: (3, 0)

    var p3 = p1 with { X = 1, Y = 4 };
    Console.WriteLine(p3);  // output: (1, 4)
}

struct record

À compter de C# 10, vous pouvez définir des types de structure d’enregistrement. Les types d’enregistrements fournissent des fonctionnalités intégrées pour encapsuler des données. Vous pouvez définir les deux types record struct et readonly record struct. Une struct d’enregistrement ne peut pas être un ref struct. Pour plus d’informations et d’exemples, consultez Enregistrements.

Tableaux inlined

À compter de C# 12, vous pouvez déclarer des tableaux inlined en tant struct que type :

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}

Un tableau inlined est une structure qui contient un bloc contigu d’éléments N du même type. Il s’agit d’un équivalent en code sécurisé de la déclaration de mémoire tampon fixe disponible uniquement dans le code non sécurisé. Un tableau inlined est un struct avec les caractéristiques suivantes :

  • Il contient un seul champ.
  • Le struct ne spécifie pas de disposition explicite.

En outre, le compilateur valide System.Runtime.CompilerServices.InlineArrayAttributel’attribut :

  • La longueur doit être supérieure à zéro (> 0).
  • Le type de cible doit être un struct.

Dans la plupart des cas, on peut accéder à un tableau en ligne comme à un tableau, à la fois pour lire et écrire des valeurs. En outre, vous pouvez utiliser les opérateurs de plage et d’index.

Il existe des restrictions minimales sur le type du champ unique d’un tableau inline. Il ne peut pas s’agir d’un type de pointeur :

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithPointer
{
    private unsafe char* _pointerElement;    // CS9184
}

mais il peut s’agir de n’importe quel type de référence ou n’importe quel type valeur :

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithReferenceType
{
    private string _referenceElement;
}

Vous pouvez utiliser des tableaux inlined avec presque n’importe quelle structure de données C#.

Les tableaux inlined sont une fonctionnalité de langage avancée. Ils sont destinés aux scénarios hautes performances où un bloc d’éléments inlined et contigu est plus rapide que d’autres structures de données alternatives. Vous pouvez en savoir plus sur les tableaux inlined à partir du speclet de fonctionnalité

Initialisation de struct et valeurs par défaut

Une variable d’un type struct contient directement les données pour ce struct. Cela crée une distinction entre une struct non initialisée qui a sa valeur par défaut, et une struct initialisée qui stocke les valeurs définies en la construisant. Par exemple, prenons le code suivant :

public readonly struct Measurement
{
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; }

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement();
    Console.WriteLine(m1);  // output: NaN (Undefined)

    var m2 = default(Measurement);
    Console.WriteLine(m2);  // output: 0 ()

    var ms = new Measurement[2];
    Console.WriteLine(string.Join(", ", ms));  // output: 0 (), 0 ()
}

Comme l’illustre l’exemple précédent, l’expression de valeur par défaut ignore un constructeur sans paramètre et produit la valeur par défaut du type de structure. L’instanciation de tableau de type structure ignore également un constructeur sans paramètre et produit un tableau rempli avec les valeurs par défaut d’un type de structure.

La situation la plus courante où vous verrez que les valeurs par défaut se trouvent dans des tableaux ou dans d’autres collections où le stockage interne inclut des blocs de variables. L’exemple suivant crée un tableau de 30 structures TemperatureRange, chacune ayant la valeur par défaut :

// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];

Tous les champs membres d’une struct doivent être définitivement attribués lors de sa création, car les types struct stockent directement leurs données. La valeur default d’un struct a définitivement défini tous les champs sur 0. Tous les champs doivent être affectés définitivement lorsqu’un constructeur est appelé. Vous initialisez des champs à l’aide des mécanismes suivants :

  • Vous pouvez ajouter des initialiseurs de champ à n’importe quel champ ou propriété implémentée automatiquement.
  • Vous pouvez initialiser tous les champs ou propriétés automatiques dans le corps du constructeur.

À compter de C# 11, si vous n’initialisez pas tous les champs dans une struct, le compilateur ajoute du code au constructeur qui initialise ces champs à la valeur par défaut. Le compilateur effectue son analyse habituelle de l’affectation définie. Tous les champs accessibles avant d’être attribués, ou qui ne sont pas affectés définitivement lorsque le constructeur termine l’exécution sont affectés à leurs valeurs par défaut avant l’exécution du corps du constructeur. Si this est accessible avant que tous les champs ne soient attribués, la struct est initialisé à la valeur par défaut avant l’exécution du corps du constructeur.

public readonly struct Measurement
{
    public Measurement(double value)
    {
        Value = value;
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public Measurement(string description)
    {
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; } = "Ordinary measurement";

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement(5);
    Console.WriteLine(m1);  // output: 5 (Ordinary measurement)

    var m2 = new Measurement();
    Console.WriteLine(m2);  // output: 0 ()

    var m3 = default(Measurement);
    Console.WriteLine(m3);  // output: 0 ()
}

Chaque struct a un constructeur public sans paramètre. Si vous écrivez un constructeur sans paramètre, il doit être public. Si une struct déclare des initialiseurs de champ, elle doit déclarer explicitement un constructeur. Ce constructeur n’a pas besoin d’être sans paramètre. Si une struct déclare un initialiseur de champ mais aucun constructeur, le compilateur signale une erreur. Tout constructeur explicitement déclaré (avec des paramètres ou sans paramètre) exécute tous les initialiseurs de champ pour cette struct. Tous les champs sans initialiseur de champ ou affectation dans un constructeur sont définis sur la valeur par défaut. Pour plus d’informations, consultez la proposition de caractéristique Constructeurs de struct sans paramètre.

À partir de C# 12, les types struct peuvent définir un constructeur principal dans le cadre de leur déclaration. Les constructeurs primaires fournissent une syntaxe concise pour les paramètres du constructeur qui peuvent être utilisés dans tout le corps, struct, dans n'importe quelle déclaration de membre pour cette struct.

Si tous les champs d’instance d’un type de structure sont accessibles, vous pouvez également l’instancier sans l’opérateur new. Dans ce cas, vous devez initialiser tous les champs d’instance avant la première utilisation de l’instance. L’exemple suivant montre comment effectuer cette opération :

public static class StructWithoutNew
{
    public struct Coords
    {
        public double x;
        public double y;
    }

    public static void Main()
    {
        Coords p;
        p.x = 3;
        p.y = 4;
        Console.WriteLine($"({p.x}, {p.y})");  // output: (3, 4)
    }
}

Dans le cas des types de valeur intégrés, utilisez les littéraux correspondants pour spécifier une valeur du type.

Limitations avec la conception d’un type de structure

Les structs ont la plupart des fonctionnalités d’un type de classe. Il existe certaines exceptions, et notamment des exceptions qui ont été supprimées dans des versions plus récentes :

  • Un type de structure ne peut pas hériter d’une autre classe ou d’un autre type de structure, et il ne peut pas être la base d’une classe. Toutefois, un type de structure peut implémenter des interfaces.
  • Vous ne pouvez pas déclarer de finaliseur au sein d’un type de structure.
  • Avant C# 11, un constructeur d’un type de structure doit initialiser tous les champs d’instance du type.

Passage de variables de type structure par référence

Lorsque vous transformez une variable de type structure en une méthode en tant qu’argument ou que vous renvoyez une valeur de type structure à partir d’une méthode, l’instance entière d’un type de structure est copiée. La transmission par valeur peut affecter les performances de votre code dans des scénarios hautes performances qui impliquent des types de structure volumineux. Vous pouvez éviter la copie de valeurs en transformant une variable de type structure par référence. Utilisez les modificateurs du paramètre de méthode ref, out, in ou ref readonly pour indiquer qu’un argument doit être transmis par référence. Utilisez les retours par référence pour retourner un résultat de méthode par référence. Pour plus d’informations, consultez Éviter les allocations.

contrainte de struct

Vous utilisez également le mot clé struct dans la contrainte struct pour spécifier qu’un paramètre de type est un type valeur non nullable. Les types de structure et d’énumération répondent à la contrainte struct.

Conversions

Pour n’importe quel type de structure (à l’exception des types ref struct), il existe des conversions boxing et unboxing vers et depuis les types System.ValueType et System.Object. Il existe également des conversions boxing et unboxing entre un type de structure et toute interface qu’il implémente.

spécification du langage C#

Pour plus d’informations, consultez la section Structs de la Spécification du langage C#.

Pour plus d’informations sur les fonctionnalités struct, consultez les notes de proposition des fonctionnalités suivantes :

Voir aussi