BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Les Records En C# 9

Les Records En C# 9

Favoris

Points Clés

  • C# 9 introduit les records, un nouveau type référence pour encapsuler les données que les développeurs peuvent utiliser à la place des classes et des structs.
  • Les instances de record peuvent avoir des propriétés immuables grâce à l'utilisation de paramètres pré-initialisés.
  • Les types Record ont une méthode ToString générée par le compilateur qui renvoie les noms et les valeurs des propriétés et des champs publics d'une instance.
  • À la différence des classes, l'égalité dans Records ne signifie pas nécessairement égalité de référence. Deux instances de record sont égales si les valeurs de toutes leurs propriétés et champs sont égales.
  • La mutation non destructive permet la création de nouvelles instances d'un record à partir de records immuables existants.
  • Les records peuvent être hérités.

Présentation des records

C# 9 introduit les records, un nouveau type référence pour encapsuler les données que les développeurs peuvent utiliser à la place des classes et des structs.

Bien que les records puissent être modifiables, le nouveau type référence est principalement destiné à être utilisé avec des modèles de données immuables. Il a les caractéristiques clés suivantes :

  • Syntaxe pour créer un type référence avec des propriétés immuables
  • Mise en forme intégrée pour l'affichage
  • Égalité basée sur les valeurs
  • Mutation non destructive avec une syntaxe concise
  • Prise en charge des hiérarchies d'héritage

La syntaxe de déclaration d'un type record est très similaire à celle utilisée par une classe, à l'exception du mot-clé utilisé pour la déclaration :

Exemple 1 : Déclaration de records et de classes

public class Pet {
	public string Name {get; set;}
	public int Age{get; set;}
}

// remplacer le mot clé “class” par “record”
public record Pet {
	public string Name {get; set;}
	public int Age{get; set;}
}

L'exemple ci-dessus montre comment déclarer un record Pet avec des getters et setters traditionnels par création nominale.

Propriété mutable

Les records sont principalement destinés à être utilisés avec des modèles de données immuables, mais ils ne sont pas nécessairement immuables.

Dans l'exemple ci-dessus, nous avons déclaré les propriétés du record avec l'accesseur set. Cela signifie que l'état de l'objet peut être modifié après sa création, les rendant mutables. Cependant, déclarer des types record avec des paramètres positionnels les rend immuables par défaut :

Exemple 2 : Déclaration avec paramètres positionnels

public record Pet(string Name, int Age);

L'immuabilité peut être utile dans des scénarios spécifiques où vous ne souhaitez autoriser aucune modification d'une variable d'un record après son initialisation. Un Data Transfer Object (DTO) serait un excellent exemple d'utilisation de l'immutabilité. L'utilisation d'un record immuable en tant que DTO garantira que l'objet n'est pas modifié lors du transfert entre une base de données et un client.

Un type record immuable est thread-safe et ne peut pas muter ou changer après sa création, bien que la mutation non destructive soit autorisée. Les types record ne peuvent être initialisés qu'à l'intérieur d'un constructeur.

Setters d'initialisation uniquement

Les setters d'initialisation uniquement sont introduits dans C#9, et ils peuvent être utilisés à la place de set pour les propriétés et les indexeurs. Les accesseurs init sont très similaires à readonly, à deux exceptions près :

  • Les propriétés avec des setters init uniquement ne peuvent être définies que dans l'initialiseur d'objet, le constructeur ou l'accesseur init ;
  • Une fois leurs valeurs définies, elles ne peuvent plus être modifiées.

En d'autres termes, init fournit une fenêtre pour changer l'état d'un record, mais toutes les propriétés utilisant des setters init uniquement deviennent en lecture seule une fois le record initialisé.

Exemple 3 : Utilisation de setters init uniquement avec une déclaration d'un record nominal

public record Pet {
	public string Name {get; init;}
	public int Age{get; init;}
} 

L'un des plus grands avantages de l'utilisation de setters init uniquement est que nous pouvons éviter les bugs qui pourraient être introduits lorsque nous passons l'argument objet par référence à une méthode et que la valeur de ses propriétés est modifiée pour une raison quelconque.

Utilisation des types record

Nous pouvons initialiser les propriétés d'un record en utilisant des paramètres positionnels (constructeurs).

Exemple 4 : Déclaration d'un type d'un record avec des paramètres positionnels

public record Pet(string Name, int Age);

L'exemple ci-dessus utilise les propriétés Name et Age en tant que paramètres positionnels (constructeur) pour le type record Pet. Mais nous pouvons également combiner des déclarations nominales et positionnelles lorsque d'autres propriétés n'ont pas nécessairement besoin d'être définies lors de l'initialisation du record.

Exemple 5 : Utilisation de la déclaration nominale et positionnelle avec des records

public record Pet(string Name, int Age)
{
	public string Color{ get; init; }
}

Dans l'exemple ci-dessus, la propriété Color n'est pas déclarée comme paramètre positionnel. Par conséquent, nous n'avons pas besoin de le définir lorsque nous créons une nouvelle instance de Pet.

Exemple 6 : Initialisation d'un type record avec déclarations nominales et positionnelles

var dog = new Pet("Cookie", 7);
var dog = new Pet("Cookie", 7){Color = “Brown”};

Formatage intégré pour l'affichage

Contrairement aux classes, la méthode du record ToString() générée par le compilateur utilise un StringBuilder pour afficher les noms et les valeurs d'une instance lorsque ses propriétés et ses champs sont publics.

Exemple 7 : Utilisation de ToString() avec un type record

var dog = new Pet("Cookie", 7){Color = “Brown”};
dog.ToString(); 

Sortie :

Pet{ Name = Cookie, Age= 7, Color = Brown}

Égalité de valeur

L'égalité des valeurs signifie que deux variables d'un type d'record sont égales si leurs définitions de type sont identiques et si pour chaque champ, les valeurs des deux records sont égales.

Par opposition, deux variables d'un type classe seront considérées comme différentes même si toutes ses propriétés sont identiques. Cela est dû au fait que les classes utilisent l'égalité de référence, ce qui signifie que deux variables ne sont égales que si elles se réfèrent au même objet.

Exemple 8 : comparaison de deux instances différentes du même type de record

var pet1 = new Pet("Cookie", "7");
var pet2 = new Pet("Cookie", "7");
 
var areEqual = pet1.Equals(pet2);

L'instruction areEqual ci-dessus renverra false pour les variables de classes en C# car elles pointent vers des objets différents, mais elle renverra true pour les records car elles stockent la même valeur de mêmes types pour chacune de leurs propriétés.

Cependant, si les variables font référence à deux types de record différents, l'instruction renverra alors false.

Exemple 9 : Création de deux types différents d'un record

public record Pet(string Name, int Age);
public record Dog(int Age, string Name): Pet(Name, Age);
 
Pet pet = new Pet("Cookie", 7);
Dog dog = new Dog(7, "Cookie");
var areEqual = pet.Equals(dog);

Dans ce cas, l'instruction areEqual renverra false car Dog et Pet ne sont pas du même type de record.

Mutation non-destructive

Par sa propre définition, les types record immuables ne permettent aucune modification une fois qu'ils sont initialisés. Prenons l'exemple d'un type record immuable avec des setters init uniquement :

Exemple 10 : Déclaration d'un type record immuable avec des setters init uniquement

public record Pet
{
	public string Name{ get; init; }
	public int Age{ get; init; }
};

var newPet = pet;
newPet.Name = "Cookie";
newPet.Age = 7;

L'extrait de code ci-dessus ne sera pas exécuté car toutes ses propriétés ont des paramètres d'initialisation uniquement : les valeurs de propriété de newPet ne peuvent pas être modifiées après l'initialisation de la variable. Par conséquent, les valeurs de propriété pour newPet ne sont pas définies.

Afin de pouvoir modifier les propriétés d'une instance d'un record immuable déjà initialisée, nous devons créer une nouvelle instance d'un record et modifier ses propriétés lors de l'instanciation. Ce processus est appelé mutation non destructive :

Exemple 11 : Modification des propriétés d'une variable d'un record existante

var pet = new Pet("Cookie", 7)
{
	Color = "Brown"
};
 
var modifiedPet = new Pet(pet.Name, pet.Age)
{
	Color = "Black"
};

Dans l'exemple ci-dessus, nous avons modifié les propriétés de la variable pet en utilisant une mutation non destructive. Nous créons une nouvelle variable modifiedPet basée sur pet, en modifiant la valeur de la propriété Color pendant le processus d'instanciation.

Nous pouvons également utiliser l'expression with pour spécifier uniquement les propriétés que nous souhaitons modifier lors de la création de la nouvelle variable :

Exemple 12 : Mutation non destructive utilisant with

public record Pet(string Name, int Age);
Pet pet = new Pet("Cookie", 7);
 
var modifiedPet = Pet with
{
Age = 10
};

Dans l'exemple ci-dessus, nous créons une copie de pet en utilisant l'expression with pour modifier la valeur de la propriété Age. Toutes les autres valeurs de propriété sont copiées à partir de pet. Il est également important de noter que l'expression with ne peut être utilisée qu'avec les types record en C# 9.

De même, nous pouvons même utiliser une syntaxe différente de with pour copier un record existant :

Exemple 13 : Copie d'une variable d'record en utilisant with

var pet1 = new Pet("Cookie", 7);
var pet2 = pet1 with{};
var areEqual = pet1.Equals(pet2);

Héritage

Un record peut hériter d'un autre record. Cependant, un record ne peut pas hériter d'une classe, et une classe ne peut pas hériter d'un record.

Exemple 14 : Héritage d'un type record²

public record Pet(string Name, int Age);
public record Dog(string Name, int Age, string Color): Pet(Name, Age);
var dog = new Dog("Cookie", 7, “Brown”);

Dans l'exemple ci-dessus, Dog est un type record hérité du type record Pet.

Utiliser des déconstructeurs

Les records prennent également en charge les déconstructeurs, qui convertiront l'instance d'un record en un tuple contenant toutes ses propriétés. Dans l'exemple ci-dessous, nous créons une instance de Dog (dog) qui est héritée de Pet, mais avec ses propriétés dans un ordre différent :

Exemple 15 : Création d'une instance héritée de Dog

public record Pet(string Name, string Color);
public record Dog(string Color, string Name): Pet(Name, Color);
 
var dog = new Dog(“Brown”, "Cookie");

Nous avons modifié l'ordre des propriétés lors de l'héritage pour montrer les conséquences de l'utilisation de la syntaxe positionnelle. La déconstruction d'une variable renverra un tuple dont les valeurs dépendront du type de l'instance à déconstruire :

Exemple 16 : Déconstruire une instance de Dog castée en Pet

string name = null;
string color = null;
 (name, color) = (Pet)dog;
 Console.WriteLine($"{name} {color}");

Sortie :

Cookie Brown

Dans l'exemple ci-dessus, nous déconstruisons le cast de dog en tant qu'instance de Pet. En conséquence, la déconstruction renverra les valeurs de propriété telles qu'elles sont déclarées dans le type Pet, dans le même ordre.

Si nous ne castons pas dog en Pet, cependant, le processus de déconstruction renverra les valeurs de propriété dans un ordre différent (après la déclaration du type record Dog) :

Exemple 17 : Déconstruire une instance de Dog

(color, name) = dog;
 Console.WriteLine($"{name} {color}");

Sortie :

Cookie Brown

Cas limites sur les types record :

Cas 1 : contournement des setters d'initialisation

L'immuabilité est l'un des avantages de l'utilisation des records en tant que types centrés sur les données. Cependant, nous pouvons toujours modifier les valeurs de propriété de n'importe quelle instance d'un record pendant l'exécution à l'aide de la reflexion, de la même manière que pour tout autre objet. Cela permet au développeur de contourner les paramètres d'initialisation uniquement utilisés lors de la déclaration d'un record :

Exemple 18 : Contourner les setters d'initialisation à l'aide de la reflection

public record Pet(string Name, int Age)
{
 	public string Color {get; init;}
};

Pet pet = new Pet("Cookie", 7)
{
Color = “Brown”
};

var propertyInfo = typeof(Pet).GetProperties()
   .FirstOrDefault(p => p.Name == nameof(pet.Color));

propertyInfo.SetValue(pet, “Black”);
Console.WriteLine(pet.Color);

Sortie :

Black

L'exemple ci-dessus montre comment nous pouvons utiliser la méthode GetProperties() pour modifier la valeur de la propriété Color d'une variable d'un record immuable même après son initialisation.

Cas 2 : Déconstruire les records à paramètre unique

Lors de la déconstruction des variables d'un record, il est nécessaire que le type du record ait au moins deux paramètres positionnels (propriétés) :

Exemple 19 : Déconstruire des records avec un seul paramètre positionnel

public record Pet(string Name);
Pet pet = new Pet("Cookie");
String name = "Something";
(name) = pet 

L'extrait de code ci-dessus ne fonctionnera pas, car le compilateur comprendra que nous essayons d'affecter un objet Pet à une variable String (même avec la parenthèse, (name) n'est pas interprété comme un tuple à paramètre unique). Comme alternative, nous pouvons définir explicitement la sortie du déconstructeur :

public record Pet(string Name);
Pet pet = new Pet("Cookie");
String name = "Something";
pet.Deconstruct(out name);

Quand utiliser le type record ?

Vous devez utiliser des types record dans votre application si :

  • Votre type de données encapsule une valeur complexe ;
  • Il n'y a qu'un seul moyen de le transférer vers d'autres parties de l'application (flux de données unidirectionnel) ;
  • Il peut être nécessaire d'utiliser des hiérarchies d'héritage.

Si votre type de données peut être un type valeur qui contient les données dans sa propre allocation de mémoire (par exemple, int, double) et qu'il doit être immuable, vous devez utiliser des structs. Les structs sont des types valeur. Les classes sont des types référence et les records sont par défaut des types référence immuables.

Les avantages de l'utilisation des types record :

  • Étant donné que l'état d'un objet immuable ne change jamais une fois qu'il est initialisé, les types record facilitent le processus de gestion de la mémoire et facilitent la maintenance et le débogage du code.
  • Les types d'record nous aident à détecter plus tôt les bugs liés aux données, et en raison des améliorations apportées à la syntaxe, la base de code sera (généralement) beaucoup plus petite et propre.

L'inconvénient des types record :

Il ne prend pas en charge IComparable, ce qui signifie que les listes de types records ne sont pas triables. Pour illustrer cette limitation, le code suivant lèvera une exception :

Exemple 20 : Tri de différentes instances d'un type record

var pet1 = new Pet("Cookie", "7");
var pet2 = new Pet("Cookie", "8");
var list = new List<Pet>
{
 	pet1, pet2
};
list.Sort();

A propos de l'auteur

Tugce Ozdeger est titulaire d'une maîtrise en informatique de l'Université d'Uppsala et a plus de 10 ans d'expérience professionnelle avec le framework .NET en tant qu'ingénieur logiciel senior basée à Stockholm, en Suède. Elle s'est principalement spécialisée dans C#.NET, les applications de bureau (WinForms, WPF), WCF, LINQ, .NET Core, SQL Server, Web Forms, ASP.NET MVC, EF Core et MVVM. Elle a un réel intérêt pour Microsoft Azure ainsi que pour l'IA. Elle est la fondatrice de Heart-Centric Tech Mentoring et une mentore d'accélération de carrière pour les femmes en technologie. Elle co-fonde actuellement une startup technologique en tant que CTO.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT