Enregistrements (référence C#)

À compter de C# 9, vous utilisez le record mot clé pour définir un type de référence qui fournit des fonctionnalités intégrées pour l’encapsulation des données. Vous pouvez créer des types d’enregistrements avec des propriétés immuables à l’aide de paramètres positionnels ou d’une syntaxe de propriété standard :

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
};

Vous pouvez également créer des types d’enregistrements avec des propriétés et des champs mutables :

public record Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
};

Alors que les enregistrements peuvent être mutables, ils sont principalement destinés à la prise en charge des modèles de données immuables. Le type d’enregistrement offre les fonctionnalités suivantes :

Vous pouvez également utiliser des types de structure pour concevoir des types centrés sur les données qui fournissent l’égalité des valeurs et peu ou pas de comportement. Toutefois, pour les modèles de données relativement volumineux, les types de structure présentent quelques inconvénients :

  • Ils ne prennent pas en charge l’héritage.
  • Elles sont moins efficaces pour déterminer l’égalité des valeurs. Pour les types valeur, la ValueType.Equals méthode utilise la réflexion pour rechercher tous les champs. Pour les enregistrements, le compilateur génère la Equals méthode. Dans la pratique, l’implémentation de l’égalité des valeurs dans les enregistrements est tangiblement plus rapide.
  • Ils utilisent davantage de mémoire dans certains scénarios, car chaque instance dispose d’une copie complète de toutes les données. Les types d’enregistrements étant des types référence, une instance d’enregistrement contient uniquement une référence aux données.

Syntaxe de position pour la définition de propriété

Vous pouvez utiliser des paramètres positionnels pour déclarer les propriétés d’un enregistrement et initialiser les valeurs de propriété lors de la création d’une instance :

public record Person(string FirstName, string LastName);

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}

Lorsque vous utilisez la syntaxe de position pour la définition de propriété, le compilateur crée :

  • Propriété publique init-only implémentée automatiquement pour chaque paramètre positionnel fourni dans la déclaration d’enregistrement. Une propriété init-only ne peut être définie que dans le constructeur ou à l’aide d’un initialiseur de propriété.
  • Constructeur principal dont les paramètres correspondent aux paramètres positionnels de la déclaration d’enregistrement.
  • Une Deconstruct méthode avec un out paramètre pour chaque paramètre positionnel fourni dans la déclaration d’enregistrement. Cette méthode est fournie uniquement s’il existe au moins deux paramètres positionnels. La méthode déconstruit des propriétés définies à l’aide de la syntaxe de position ; elle ignore les propriétés définies à l’aide de la syntaxe de propriété standard.

Vous pouvez ajouter des attributs à l’un de ces éléments que le compilateur crée à partir de la définition d’enregistrement. Vous pouvez ajouter une cible à n’importe quel attribut que vous appliquez aux propriétés de l’enregistrement positionnel. L’exemple suivant applique System.Text.Json.Serialization.JsonPropertyNameAttribute à chaque propriété de l' Person enregistrement. La property: cible indique que l’attribut est appliqué à la propriété générée par le compilateur. D’autres valeurs sont field: l’application de l’attribut au champ et l' param: application de l’attribut au paramètre.

/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName, 
    [property: JsonPropertyName("lastName")]string LastName);

L’exemple précédent montre également comment créer des commentaires de documentation XML pour l’enregistrement. Vous pouvez ajouter la <param> balise pour ajouter de la documentation pour les paramètres du constructeur principal.

Si la définition de propriété implémentée automatiquement générée n’est pas celle que vous souhaitez, vous pouvez définir votre propre propriété du même nom. Dans ce cas, le constructeur et le deconstructeur générés utilisent votre définition de propriété. Par exemple, l’exemple suivant définit la FirstName propriété positionnel internal au lieu de public .

public record Person(string FirstName, string LastName)
{
    internal string FirstName { get; init; } = FirstName;
}

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person.FirstName); //output: Nancy
}

Un type d’enregistrement n’a pas à déclarer de propriétés positionnelles. Vous pouvez déclarer un enregistrement sans propriétés positionnelles et vous pouvez déclarer des champs et des propriétés supplémentaires, comme dans l’exemple suivant :

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
};

Si vous définissez des propriétés à l’aide de la syntaxe de propriété standard mais omettez le modificateur d’accès, les propriétés sont implicitement private .

Immuabilité

Un type d’enregistrement n’est pas nécessairement immuable. Vous pouvez déclarer des propriétés avec des set accesseurs et des champs qui ne le sont pas readonly . Toutefois, bien que les enregistrements puissent être mutables, ils facilitent la création de modèles de données immuables.

L’immuabilité peut être utile lorsque vous avez besoin d’un type centré sur les données pour être thread-safe ou que vous utilisez un code de hachage restant identique dans une table de hachage. Toutefois, l’immuabilité n’est pas appropriée pour tous les scénarios de données. Par exemple, Entity Framework Corene prend pas en charge la mise à jour avec des types d’entité immuables.

Les propriétés init-only, qu’elles soient créées à partir de paramètres positionnels ou en spécifiant des init accesseurs, présentent un immuabilité superficielle. Après l’initialisation, vous ne pouvez pas modifier la valeur des propriétés de type valeur ou la référence des propriétés de type référence. Toutefois, les données auxquelles une propriété de type référence fait référence peuvent être modifiées. L’exemple suivant montre que le contenu d’une propriété immuable de type référence (un tableau dans ce cas) est mutable :

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234

    person.PhoneNumbers[0] = "555-6789";
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}

Les fonctionnalités propres aux types d’enregistrements sont implémentées par les méthodes synthétisées par le compilateur, et aucune de ces méthodes ne compromet l’immuabilité en modifiant l’état de l’objet.

Égalité des valeurs

L’égalité des valeurs signifie que deux variables d’un type d’enregistrement sont égales si les types correspondent et que toutes les valeurs de propriété et de champ correspondent. Pour les autres types de référence, l’égalité correspond à l’identité. Autrement dit, deux variables d’un type référence sont égales si elles font référence au même objet.

L’égalité des références est requise pour certains modèles de données. Par exemple, Entity Framework Core dépend de l’égalité des références pour garantir qu’il n’utilise qu’une seule instance d’un type d’entité pour ce qui est conceptuellement une entité. Pour cette raison, les types d’enregistrements ne sont pas appropriés pour une utilisation en tant que types d’entité dans Entity Framework Core.

L’exemple suivant illustre l’égalité des valeurs des types d’enregistrements :

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    var phoneNumbers = new string[2];
    Person person1 = new("Nancy", "Davolio", phoneNumbers);
    Person person2 = new("Nancy", "Davolio", phoneNumbers);
    Console.WriteLine(person1 == person2); // output: True

    person1.PhoneNumbers[0] = "555-1234";
    Console.WriteLine(person1 == person2); // output: True

    Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}

Pour implémenter l’égalité des valeurs, le compilateur synthétise les méthodes suivantes :

Dans class les types, vous pouvez substituer manuellement les méthodes et les opérateurs d’égalité pour atteindre l’égalité des valeurs, mais le développement et le test de ce code seraient fastidieux et sujets aux erreurs. Le fait de disposer de cette fonctionnalité permet d’éviter les bogues susceptibles d’oublier de mettre à jour le code de remplacement personnalisé lors de l’ajout ou de la modification des propriétés ou des champs.

Vous pouvez écrire vos propres implémentations pour remplacer n’importe laquelle de ces méthodes synthétisées. Si un type d’enregistrement a une méthode qui correspond à la signature d’une méthode synthétisée, le compilateur ne synthétise pas cette méthode.

Si vous fournissez votre propre implémentation de Equals dans un type d’enregistrement, fournissez également une implémentation de GetHashCode .

Mutation non destructrice

Si vous devez muter des propriétés immuables d’une instance d’enregistrement, vous pouvez utiliser une with expression pour obtenir une mutation non destructrice. Une with expression crée une nouvelle instance d’enregistrement qui est une copie d’une instance d’enregistrement existante, avec les propriétés et les champs spécifiés modifiés. Utilisez la syntaxe de l' initialiseur d’objet pour spécifier les valeurs à modifier, comme indiqué dans l’exemple suivant :

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
}

public static void Main()
{
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

    Person person2 = person1 with { FirstName = "John" };
    Console.WriteLine(person2);
    // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { PhoneNumbers = new string[1] };
    Console.WriteLine(person2);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { };
    Console.WriteLine(person1 == person2); // output: True
}

L' with expression peut définir des propriétés positionnelles ou des propriétés créées à l’aide de la syntaxe de propriété standard. Les propriétés non positionnelles doivent avoir un init set accesseur ou pour être modifiés dans une with expression.

Le résultat d’une with expression est une copie superficielle, ce qui signifie que seule la référence à une instance est copiée pour une propriété de référence. L’enregistrement d’origine et la copie se terminent par une référence à la même instance.

Pour implémenter cette fonctionnalité, le compilateur synthétise une méthode Clone et un constructeur de copie. La méthode de clonage virtuel retourne un nouvel enregistrement initialisé par le constructeur de copie. Lorsque vous utilisez une with expression, le compilateur crée du code qui appelle la méthode Clone, puis définit les propriétés spécifiées dans l' with expression.

Si vous avez besoin d’un comportement de copie différent, vous pouvez écrire votre propre constructeur de copie. Dans ce cas, le compilateur ne synthétisera pas un. Créez votre constructeur private si l’enregistrement est sealed , sinon faites-le protected .

Vous ne pouvez pas remplacer la méthode Clone et vous ne pouvez pas créer un membre nommé Clone . Le nom réel de la méthode Clone est généré par le compilateur.

Mise en forme intégrée pour l’affichage

Les types d’enregistrements ont une méthode générée par ToString le compilateur qui affiche les noms et les valeurs des propriétés et des champs publics. La ToString méthode retourne une chaîne au format suivant :

<record type name> { <property name> = <value>, <property name> = <value>, ...}

Pour les types référence, le nom de type de l’objet auquel la propriété fait référence s’affiche à la place de la valeur de la propriété. Dans l’exemple suivant, le tableau est un type référence, donc System.String[] s’affiche à la place des valeurs d’élément de tableau réelles :

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Pour implémenter cette fonctionnalité, le compilateur synthétise une PrintMembers méthode virtuelle et une ToString substitution. La ToString substitution crée un StringBuilder objet avec le nom de type suivi d’un crochet ouvrant. Elle appelle PrintMembers pour ajouter des noms et des valeurs de propriété, puis ajoute le crochet fermant. L’exemple suivant montre un code similaire à ce que contient la substitution synthétisée :

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("Teacher"); // type name
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Vous pouvez fournir votre propre implémentation de PrintMembers ou du ToString remplacement. Des exemples sont fournis dans la section PrintMembers mise en forme des enregistrements dérivés plus loin dans cet article. Dans C# 10 et versions ultérieures, votre implémentation de ToString peut inclure le sealed modificateur, ce qui empêche le compilateur de synthétiser une ToString implémentation pour tous les enregistrements dérivés. En fait, cela signifie que la ToString sortie n’inclut pas les informations de type au moment de l’exécution. (Tous les membres et valeurs sont affichés, car une méthode PrintMembers est toujours générée pour les enregistrements dérivés.)

Héritage

Un enregistrement peut hériter d’un autre enregistrement. Toutefois, un enregistrement ne peut pas hériter d’une classe, et une classe ne peut pas hériter d’un enregistrement.

Paramètres positionnels dans les types d’enregistrements dérivés

L’enregistrement dérivé déclare des paramètres positionnels pour tous les paramètres dans le constructeur principal d’enregistrement de base. L’enregistrement de base déclare et initialise ces propriétés. L’enregistrement dérivé ne les masque pas, mais crée et initialise uniquement les propriétés des paramètres qui ne sont pas déclarés dans son enregistrement de base.

L’exemple suivant illustre l’héritage avec la syntaxe de propriété de position :

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Égalité dans les hiérarchies d’héritage

Pour que deux variables d’enregistrement soient égales, le type au moment de l’exécution doit être égal. Les types des variables conteneur peuvent être différents. Cela est illustré dans l’exemple de code suivant :

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(student2 == student); // output: True
}

Dans l’exemple, toutes les instances ont les mêmes propriétés et les mêmes valeurs de propriété. Mais student == teacher retourne False , bien que les deux Person variables sont de type, et student == student2 retourne, True bien qu’une variable soit une Person variable et une Student variable.

Pour implémenter ce comportement, le compilateur synthétise une EqualityContract propriété qui retourne un Type objet qui correspond au type de l’enregistrement. Cela permet aux méthodes d’égalité de comparer le type d’exécution des objets lorsqu’ils vérifient l’égalité. Si le type de base d’un enregistrement est object , cette propriété est virtual . Si le type de base est un autre type d’enregistrement, cette propriété est une substitution. Si le type d’enregistrement est sealed , cette propriété est sealed .

Lors de la comparaison de deux instances d’un type dérivé, les méthodes d’égalité synthétisée vérifient l’égalité de toutes les propriétés des types de base et dérivés. La méthode synthétisée GetHashCode utilise la GetHashCode méthode de toutes les propriétés et les champs déclarés dans le type de base et le type d’enregistrement dérivé.

with expressions dans les enregistrements dérivés

Le résultat d’une with expression a le même type au moment de l’exécution que l’opérande de l’expression. Toutes les propriétés du type au moment de l’exécution sont copiées, mais vous pouvez définir uniquement les propriétés du type au moment de la compilation, comme le montre l’exemple suivant :

public record Point(int X, int Y)
{
    public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
    public int Zderived { get; set; }
};

public static void Main()
{
    Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };

    Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
    Console.WriteLine(p2 is NamedPoint);  // output: True
    Console.WriteLine(p2);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }

    Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
    Console.WriteLine(p3);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}

PrintMembers mise en forme dans les enregistrements dérivés

La méthode synthétisée PrintMembers d’un type d’enregistrement dérivé appelle l’implémentation de base. Le résultat est que toutes les propriétés publiques et les champs des types dérivés et de base sont inclus dans la ToString sortie, comme illustré dans l’exemple suivant :

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Vous pouvez fournir votre propre implémentation de la PrintMembers méthode. Dans ce cas, utilisez la signature suivante :

  • Pour un sealed enregistrement qui dérive de object (ne déclare pas d’enregistrement de base) : private bool PrintMembers(StringBuilder builder) ;
  • Pour un sealed enregistrement qui dérive d’un autre enregistrement : protected sealed override bool PrintMembers(StringBuilder builder) ;
  • Pour un enregistrement qui n’est pas sealed et dérive de l’objet : protected virtual bool PrintMembers(StringBuilder builder);
  • Pour un enregistrement qui n’est pas sealed et qui dérive d’un autre enregistrement : protected override bool PrintMembers(StringBuilder builder);

Voici un exemple de code qui remplace les méthodes synthétisées PrintMembers , une pour un type d’enregistrement qui dérive de Object et une pour un type d’enregistrement qui dérive d’un autre enregistrement :

public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
    protected virtual bool PrintMembers(StringBuilder stringBuilder)
    {
        stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
        stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
        return true;
    }
}

public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
    : Person(FirstName, LastName, PhoneNumbers)
{
    protected override bool PrintMembers(StringBuilder stringBuilder)
    {
        if (base.PrintMembers(stringBuilder))
        {
            stringBuilder.Append(", ");
        };
        stringBuilder.Append($"Grade = {Grade}");
        return true;
    }
};

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}

Notes

Dans C# 10,0 et versions ultérieures, le compilateur synthétisera PrintMembers lorsqu’un enregistrement de base a scellé la ToString méthode. Vous pouvez également créer votre propre implémentation de PrintMembers .

Comportement de la déconstruction dans les enregistrements dérivés

La Deconstruct méthode d’un enregistrement dérivé retourne les valeurs de toutes les propriétés positionnelles du type au moment de la compilation. Si le type de variable est un enregistrement de base, seules les propriétés d’enregistrement de base sont déconstruites, sauf si l’objet est casté en type dérivé. L’exemple suivant illustre l’appel d’un destructor sur un enregistrement dérivé.

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
    Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio

    var (fName, lName, grade) = (Teacher)teacher;
    Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}

Contraintes génériques

Il n’existe aucune contrainte générique qui requiert un type comme enregistrement. Les enregistrements répondent à la class contrainte. Pour créer une contrainte sur une hiérarchie spécifique de types d’enregistrements, placez la contrainte sur l’enregistrement de base comme vous le feriez pour une classe de base. Pour plus d’informations, consultez contraintes sur les paramètres de type.

spécification du langage C#

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

Pour plus d’informations sur les fonctionnalités introduites dans C# 9 et versions ultérieures, consultez les notes de proposition de fonctionnalités suivantes :

Voir aussi