Créer des types d’enregistrements

Les enregistrements sont des types qui utilisent l’égalité basée sur la valeur. C# 10 ajoute des structs d’enregistrement pour que vous puissiez définir des enregistrements en tant que types valeur. Deux variables d’un type d’enregistrement sont égales si les définitions de type d’enregistrement sont identiques et, si pour chaque champ, les valeurs des deux enregistrements sont égales. Deux variables d’un type de classe sont égales si les objets référencés sont le même type de classe et que les variables font référence au même objet. L’égalité basée sur la valeur implique d’autres fonctionnalités que vous souhaiterez probablement dans les types d’enregistrements. Le compilateur génère un grand nombre de ces membres lorsque vous déclarez un record au lieu d’une class. Le compilateur génère ces mêmes méthodes pour les types record struct.

Dans ce tutoriel, vous apprendrez à :

  • Déterminez si vous souhaitez ajouter le modificateur record à un type class.
  • Déclarez les types d’enregistrements et les types d’enregistrements positionnels.
  • Remplacez vos méthodes par les méthodes générées par le compilateur dans les enregistrements.

Prérequis

Vous devez configurer votre ordinateur pour exécuter .NET 6 ou version ultérieure, avec le compilateur C# 10 ou version ultérieure. Le compilateur C# 10 est disponible à partir de Visual Studio 2022 ou du Kit de développement logiciel (SDK).NET 6.

Caractéristiques des enregistrements

On définit un enregistrement en déclarant un type avec le mot clé record, en modifiant une déclaration de class ou de struct. Si vous le souhaitez, vous pouvez omettre le mot clé class afin de créer une record class. Un enregistrement suit la sémantique d’égalité basée sur la valeur. Pour appliquer la sémantique des valeurs, le compilateur génère plusieurs méthodes pour votre type d’enregistrement (à la fois pour les types record class et les types record struct) :

Les enregistrements fournissent également un remplacement de Object.ToString(). Le compilateur synthétise les méthodes d’affichage des enregistrements à l’aide de Object.ToString(). Vous allez explorer ces membres lorsque vous écrivez le code de ce didacticiel. Les enregistrements prennent en charge les expressions with pour activer la mutation non destructrice des enregistrements.

Vous pouvez également déclarer des enregistrements positionnels à l’aide d’une syntaxe plus concise. Le compilateur synthétise plus de méthodes pour vous lorsque vous déclarez des enregistrements positionnels :

  • Un constructeur principal dont les paramètres correspondent aux paramètres positionnels sur la déclaration d’enregistrement.
  • Des propriétés publiques pour chaque paramètre d’un constructeur principal. Ces propriétés sont init uniquement pour les types record class et les types readonly record struct. Pour les types record struct, elles sont en lecture-écriture.
  • Une méthode Deconstruct permettant d’extraire les propriétés de l’enregistrement.

Générer des données de température

Les données et les statistiques font partie des scénarios dans lesquels vous souhaitez utiliser des enregistrements. Pour ce didacticiel, vous allez créer une application qui calcule les degrés jours pour différentes utilisations. Les degrés jours sont une mesure de la chaleur (ou de l’absence de chaleur) sur une période de jours, de semaines ou de mois. Les degrés jours permettent de suivre et de prévoir la consommation d’énergie. Des jours plus chauds signifient plus de climatisation, et des jours plus froids signifient plus d’utilisation de la chaudière. Les degrés jours aident à gérer les populations de plantes et à mettre en corrélation la croissance des plantes au fil des saisons. Les degrés jours permettent de suivre les migrations animales pour les espèces qui se déplacent en fonction du climat.

La formule est basée sur la température moyenne sur un jour donné et une température de référence. Pour calculer les degrés jours au fil du temps, vous aurez besoin de la température maximale et minimale quotidienne pendant une période de temps. Commençons par créer une application. Créer une application console. Créez un type d’enregistrement dans un nouveau fichier nommé « DailyTemperature.cs » :

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

Le code précédent définit un enregistrement positionnel. L’enregistrement DailyTemperature est une readonly record struct, car vous n’avez pas l’intention d’en hériter et il doit être immuable. Les propriétés HighTemp et LowTemp sont des propriétés init uniquement, ce qui signifie qu’elles peuvent être définies dans le constructeur ou à l’aide d’un initialiseur de propriété. Si vous souhaitez que les paramètres positionnels soient en lecture-écriture, vous déclarez une record struct au lieu d’une readonly record struct. Le type DailyTemperature a également un constructeur principal qui a deux paramètres qui correspondent aux deux propriétés. Vous utilisez le constructeur principal pour initialiser un enregistrement de DailyTemperature. Le code suivant crée et initialise plusieurs enregistrements de DailyTemperature. Le premier utilise des paramètres nommés pour clarifier le HighTemp et LowTemp. Les initialiseurs restants utilisent des paramètres positionnels pour initialiser la HighTemp et la LowTemp :

private static DailyTemperature[] data = [
    new DailyTemperature(HighTemp: 57, LowTemp: 30), 
    new DailyTemperature(60, 35),
    new DailyTemperature(63, 33),
    new DailyTemperature(68, 29),
    new DailyTemperature(72, 47),
    new DailyTemperature(75, 55),
    new DailyTemperature(77, 55),
    new DailyTemperature(72, 58),
    new DailyTemperature(70, 47),
    new DailyTemperature(77, 59),
    new DailyTemperature(85, 65),
    new DailyTemperature(87, 65),
    new DailyTemperature(85, 72),
    new DailyTemperature(83, 68),
    new DailyTemperature(77, 65),
    new DailyTemperature(72, 58),
    new DailyTemperature(77, 55),
    new DailyTemperature(76, 53),
    new DailyTemperature(80, 60),
    new DailyTemperature(85, 66) 
];

Vous pouvez ajouter vos propres propriétés ou méthodes aux enregistrements, y compris des enregistrements positionnels. Vous devez calculer la température moyenne pour chaque jour. Vous pouvez ajouter cette propriété à l’enregistrement DailyTemperature :

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
    public double Mean => (HighTemp + LowTemp) / 2.0;
}

Assurons-nous que vous pouvez utiliser ces données. Ajoutez le code suivant à votre méthode Main :

foreach (var item in data)
    Console.WriteLine(item);

Exécutez votre application et vous verrez un résultat semblable à l’affichage suivant (plusieurs lignes supprimées pour l’espace) :

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }
DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }


DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }
DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

Le code précédent montre le résultat du remplacement de ToString synthétisé par le compilateur. Si vous préférez un texte différent, vous pouvez écrire votre propre version de ToString qui empêche le compilateur de synthétiser une version pour vous.

Calculer les degrés jours

Pour calculer les degrés jours, vous prenez la différence entre une température de référence et la température moyenne d’un jour donné. Pour mesurer la chaleur au fil du temps, vous ignorez tous les jours où la température moyenne est inférieure à la référence. Pour mesurer le froid au fil du temps, vous ignorez tous les jours où la température moyenne est supérieure à la référence. Par exemple, les États-Unis utilisent 65 F (18,3° C) comme référence pour les degrés jours de chauffage et de climatisation. C’est la température à laquelle il n’est pas nécessaire d’utiliser le chauffage ou la climatisation. Si un jour a une température moyenne de 70 F (21,1° C), ce jour nécessite cinq degrés jours de climatisation et zéro degré jour de chauffage. À l’inverse, si la température moyenne est de 55 F (12,7° C), ce jour nécessite 10 degrés jours de chauffage et 0 degré jour de climatisation.

Vous pouvez exprimer ces formules sous la forme d’une petite hiérarchie de types d’enregistrements : un type de degré jour abstrait et deux types de degré jour concrets pour les degrés jours de chauffage et les degrés jours de climatisation. Ces types peuvent également être des enregistrements positionnels. Ils prennent une température de référence et une séquence d’enregistrements de température quotidiens comme arguments pour le constructeur principal :

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}

L’enregistrement de DegreeDays abstrait est la classe de base partagée pour à la fois les enregistrements de HeatingDegreeDays et les enregistrements de CoolingDegreeDays. Les déclarations du constructeur principal sur les enregistrements dérivés montrent comment gérer l’initialisation des enregistrements de référence. Votre enregistrement dérivé déclare des paramètres pour tous les paramètres du constructeur principal de l’enregistrement de référence. L’enregistrement de référence 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 référence. Dans cet exemple, les enregistrements dérivés n’ajoutent pas de nouveaux paramètres de constructeur principal. Testez votre code en ajoutant le code suivant à votre méthode Main :

var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);

Vous obtiendrez un résultat similaire à l’affichage suivant :

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }
CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

Définir des méthodes synthétisées par le compilateur

Votre code calcule le nombre correct de degrés jours de chauffage et de climatisation au cours de cette période. Toutefois, cet exemple montre pourquoi vous souhaiterez peut-être remplacer certaines des méthodes synthétisées pour les enregistrements. Vous pouvez déclarer votre propre version de l’une des méthodes synthétisées par le compilateur dans un type d’enregistrement, à l’exception de la méthode clone. La méthode clone a un nom généré par le compilateur et vous ne pouvez pas fournir d’implémentation différente. Ces méthodes synthétisées incluent un constructeur de copie, les membres de l’interface System.IEquatable<T>, l’égalité et les tests d’inégalité, et GetHashCode(). À cet effet, vous allez synthétiser PrintMembers. Vous pouvez également déclarer votre propre ToString, mais PrintMembers offre une meilleure option pour les scénarios d’héritage. Pour fournir votre propre version d’une méthode synthétisée, la signature doit correspondre à la méthode synthétisée.

L’élément TempRecords du résultat de la console n’est pas utile. Il affiche le type, mais rien d’autre. Vous pouvez modifier ce comportement en fournissant votre propre implémentation de la méthode synthétisée PrintMembers. La signature dépend des modificateurs appliqués à la déclaration record :

  • Si un type d’enregistrement est sealed ou une record struct, la signature est private bool PrintMembers(StringBuilder builder);
  • Si un type d’enregistrement n’est pas sealed et dérive de object (autrement dit, il ne déclare pas d’enregistrement de référence), la signature est protected virtual bool PrintMembers(StringBuilder builder);
  • Si un type d’enregistrement n’est pas sealed et dérive d’un autre enregistrement, la signature est protected override bool PrintMembers(StringBuilder builder);

Ces règles sont plus faciles à comprendre par le biais de la compréhension de l’objectif de PrintMembers. PrintMembers ajoute des informations sur chaque propriété d’un type d’enregistrement à une chaîne. Le contrat nécessite des enregistrements de référence pour ajouter leurs membres à l’affichage et suppose que les membres dérivés ajouteront leurs membres. Chaque type d’enregistrement synthétise un remplacement de ToString semblable à l’exemple suivant pour HeatingDegreeDays :

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("HeatingDegreeDays");
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Vous déclarez une méthode PrintMembers dans l’enregistrement DegreeDays qui n’imprime pas le type de la collection :

protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
    stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
    return true;
}

La signature déclare une méthode virtual protected pour correspondre à la version du compilateur. Ne vous inquiétez pas si vous recevez mal les accesseurs ; le langage applique la signature correcte. Si vous oubliez les modificateurs corrects pour n’importe quelle méthode synthétisée, le compilateur émet des avertissements ou des erreurs qui vous aident à obtenir la bonne signature.

En C# 10 et versions ultérieures, vous pouvez déclarer la méthode ToString comme sealed dans un type d’enregistrement. Cela empêche les enregistrements dérivés de fournir une nouvelle implémentation. Les enregistrements dérivés contiendront toujours le remplacement de PrintMembers. Vous devez sceller ToString si vous ne souhaitez pas qu’il affiche le type d’exécution de l’enregistrement. Dans l’exemple précédent, vous perdez les informations sur l’endroit où l’enregistrement mesure les degrés jours de chauffage ou de climatisation.

Mutation non destructrice

Les membres synthétisés dans une classe d’enregistrement positionnel ne modifient pas l’état de l’enregistrement. L’objectif est que vous pouvez créer plus facilement des enregistrements immuables. N’oubliez pas que vous déclarez une readonly record struct pour créer une struct d’enregistrement immuable. Examinez à nouveau les déclarations précédentes pour HeatingDegreeDays et CoolingDegreeDays. Les membres ajoutés effectuent des calculs sur les valeurs de l’enregistrement, mais ne mutent pas l’état. Les enregistrements positionnels facilitent la création de types de référence immuables.

La création de types de référence immuables signifie que vous souhaiterez utiliser une mutation non destructrice. Vous créez de nouvelles instances d’enregistrement similaires aux instances d’enregistrement existantes à l’aide d’expressions with. Ces expressions sont une construction de copie avec des affectations supplémentaires qui modifient la copie. Le résultat est une nouvelle instance d’enregistrement où chaque propriété a été copiée à partir de l’enregistrement existant et éventuellement modifiée. L’enregistrement d’origine n’est pas modifié.

Ajoutons quelques fonctionnalités à votre programme qui illustrent les expressions with. Tout d’abord, créons un enregistrement pour calculer des degrés jours de croissance à l’aide des mêmes données. Les degrés jours de croissance utilisent généralement 41 F (5° C) comme base de référence et mesure les températures au-dessus de la référence. Pour utiliser les mêmes données, vous pouvez créer un enregistrement similaire aux coolingDegreeDays, mais avec une température de référence différente :

// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

Vous pouvez comparer le nombre de degrés calculés aux nombres générés avec une température de référence plus élevée. N’oubliez pas que les enregistrements sont des types de référence et que ces copies sont des copies superficielles. Le tableau des données n’est pas copié, mais les deux enregistrements font référence aux mêmes données. Il s’agit d’un avantage dans un autre scénario. Pour les degrés jours de croissance, il est utile de suivre le total des cinq jours précédents. Vous pouvez créer de nouveaux enregistrements avec différentes données sources à l’aide d’expressions with. Le code suivant génère une collection de ces accumulations, puis affiche les valeurs :

// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
    var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
    movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
    Console.WriteLine(item);
}

Vous pouvez également utiliser des expressions with pour créer des copies d’enregistrements. Ne spécifiez aucune propriété entre les accolades de l’expression with. Cela signifie que vous devez créer une copie et ne pas modifier de propriétés :

var growingDegreeDaysCopy = growingDegreeDays with { };

Exécutez l’application terminée pour afficher les résultats.

Résumé

Ce didacticiel a montré plusieurs aspects d’enregistrements. Les enregistrements fournissent une syntaxe concise pour les types où l’utilisation fondamentale stocke des données. Pour les classes orientées objet, l’utilisation fondamentale définit les responsabilités. Ce didacticiel s’est concentré sur les enregistrements positionnels, où vous pouvez utiliser une syntaxe concise pour déclarer les propriétés d’un enregistrement. Le compilateur synthétise plusieurs membres de l’enregistrement pour copier et comparer les enregistrements. Vous pouvez ajouter tous les autres membres dont vous avez besoin pour vos types d’enregistrements. Vous pouvez créer des types d’enregistrements immuables sachant qu’aucun des membres générés par le compilateur ne mutera l’état. Et les expressions with facilitent la prise en charge des mutations non destructrices.

Les enregistrements ajoutent une autre façon de définir des types. Vous utilisez des définitions class pour créer des hiérarchies orientées objet qui se concentrent sur les responsabilités et le comportement des objets. Vous créez des types de struct pour les structures de données qui stockent les données et sont suffisamment petits pour les copier efficacement. Vous créez des types d’record lorsque vous souhaitez une égalité et une comparaison basées sur des valeurs, utiliser des variables de référence et ne souhaitez pas copier de valeurs. Vous créez des types de record struct lorsque vous souhaitez que les fonctionnalités des enregistrements pour un type qui est assez petit pour être copié efficacement.

Vous pouvez en savoir plus sur les enregistrements dans l’article de référence du langage C# pour le type d’enregistrement et la spécification de type d’enregistrement proposée et la spécification de struct d’enregistrement.