Logo Csharp

C# 9 – les ‘Records’

Présentation de la nouvelle fonctionnalité dans la version de C# 9.0 de .NET 5.0 que sont les enregistrements ou records.

Utilisation

On peut déjà dire que ce type est un type référence immuable et semblable aux classes, structs, classes anonymes mais qu’il apporte en plus un nombre de méthodes dites synthétisées ou pré-fabriquées lors de la compilation.

Le type est record est basé sur l’équalité de valeurs. Cela signifie que deux records sont identiques quand :

  • les types des deux records sont équivalents ;
  • les valeurs de chaque attributs sont identiques.

A contrario de l’équalité de classe qui sont identiques si les deux références d’instances sont égales et donc référence la même instance et les variables contenues aussi.

Mot clé

Un nouveau mot clé est donc utilisé pour la déclaration d’un type record :

record

Syntaxe

Voici un exemple de la déclaration d’un record :

public record Vector3d
{
    public int CoordX { get; }
    public int CoordY { get; }
    public int CoordZ { get; }

    public Vector3d(int coordX, int coordY, int coordZ) => (CoordX, CoordY, CoordZ)  = (coordX, coordY, coordZ);
}

Nous venons, donc, de déclarer un record avec trois propriétés en lecteur uniquement ReadOnly. Ces valeurs sont initialisés via le constructeur. Vector3d est un type par référence. Donc pas de changement par rapport à une classe.

Tout comme les classes, les records prennent en chargent l’héritage. Donc on peut écrire la relation d’héritage suivante :

public record Vector3d
{
    public int CoordX { get; }
    public int CoordY { get; }
    public int CoordZ { get; }

    public Vector3d(int coordX, int coordY, int coordZ) => (CoordX, CoordY, CoordZ) = (coordX, coordY, coordZ);
}

public record Vector3dColorized : Vector3d
{
    public Color Colorization { get; }

    public Vector3dColorized(int coordX, int coordY, int coordZ, Color color) 
    : base (coordX, coordY, coordZ) => (Colorization) = (color);
}

On peut aussi rendre sealed un record.

Soyons un peu plus curieux est regardons de plus prêt le code IL généré par le compilateur.

Code IL

Le record est une classe

.class nested public auto ansi beforefieldinit
    Vector3d

Classe immuable

Nos propriétés sont en READONLY et donc non modifiable après sa création. IL d’une propriété.

.method public hidebysig specialname instance int32
    get_CoordX() cil managed
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    = (01 00 00 00 )
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: ldfld        int32 csharpnine_record.Program/Vector3d::'<CoordX>k__BackingField'
    IL_0006: ret

Méthodes pré-construites ou synthétisées

La nouveauté est que le compilateur a construit un ensemble de méthodes en plus de celles du développeur qui vont permettre et aider dans le traitement par valeurs du record et facilité la création des enregistrements. Il faut retenir aussi que ces méthodes synthétisées n’altérent en rien l’état de l’enregistrement.

Ces méthodes sont les suivantes :

  • méthode(s) pour la comparaison par valeurs ;
  • méthode(s) pour le calcul du code de hashage de l’objet ;
  • méthode(s) pour cloner et copier ;
  • méthode(s) d’impression des membres.

Opérateurs de comparaisons

Les opérateurs d’égalités == et d’inégalités != ont été surchargés pour respecter le principe d’égalité par valeurs des records.

Pour rappel cette égalité se fait sur les valeurs et sur les types. Donc, dans notre exemple, un Vector3dColorized n’est pas égal à un Vector3d.

Interface IEquatable<T>

Par défault, le type enregistrement synthétise cette interface.

EqualityContract()

Permet de retourne le type de l’enregistrement typeof(R).

Le prototype est le suivant dans le cas d’un héritage d’un type d’enregistrement de base :

protected override Type EqualityContract { get; };

ToString()

Combien de fois, nous avons surchagé cette méthode pour retourner formaté le contenu de notre objet, pour le débuggueur puisse correctement afficher le contenu de l’objet et non pas son type.

Dorénavant le record le fait pour nous à la compilation. Cela nous permet d’avoir sous forme de chaine de caractères l’objet et ses valeurs formatées.

La méthode ToString() est synthétisée, elle appelle en interne la méthode PrintMembers() et formatte la chaine avant de la retourner.

Reprenons nos exemples, cela donnera la représentation suivante :

Vector3d v3d = new Vector3d(10,20,30);
Vector3dColorized v3dc = new Vector3dColorized(10,20,30, Color.Blue);

Console.WriteLine(v3d.ToString());  // le ToString() n'est pas obligatoire ici
Console.WriteLine(v3dc.ToString()); // c'est pour l'exemple.

Résultat

Vector3d { CoordX = 10, CoordY = 20, CoordZ = 30 }
Vector3dColorized { CoordX = 10, CoordY = 20, CoordZ = 30, Colorization = Color [Blue] }

PrintMembers(StringBuilder)

Cette méthode formatte les différentes valeurs de la hiérarchie d’objets et formatte dans une liste séparée par des virgules les noms et valeurs.

Elle n’est pas exposée publiquement. Mais du prototype suivant :

protected override bool PrintMembers(StringBuilder builder);

Dans notre exemple précédent cela donnera pour Vector3dColorized la chaine suivante :

CoordX = 10, CoordY = 20, CoordZ = 30, Colorization = Color [Blue]

GetHashCode()

Calcul le Hash d’un objet basé sur les valeurs de celui-ci. Donc on peut avoir deux Hash équivalent pour deux instances d’objets différents si ils contiennent les mêmes valeurs.

Cloner, Copier

Pour les enregistrements il existe deux méthodes pour effectuer sa copie :

  • Méthode clone non publique et synthéthisée par le compilateur ;
  • Constructeur avec un argument du type de l’enregistrement.

Mutation non destructive

La mutation non destructive va permettre la création d’un enregistrement similaire à celui d’origine mais en laissant la possibilité d’effectuer des modifications sur les valeurs de l’enregistrement de destination/la copie. On parle de construction de copie.

L’enregistrement d’origine reste inchangé.

Les expressions with

Pour se faire nous allons utiliser le mot clé with.

Les expressions with permettent d’effectuer des copies d’enregistrement avec possibilités de modifier les champs et les propriétés.

Un exemple étant plus parlant, nous allons reprendre la classe Vectod3d et en faire deux copies, une identique et une en modifiant une propriété. Il faudra penser à modifier les assesseurs des propriétés en get ET set dans notre exemple.

Vector3d v3d = new Vector3d(10,20,30);
Vector3d v3dCopy = v3d with { };
Vector3d v3dCopyDifferent = v3d with { CoordX = 100 };

Console.WriteLine(v3d);
Console.WriteLine(v3dCopy);
Console.WriteLine(v3dCopyDifferent);

Console.WriteLine((v3d == v3dCopy)?"Doublon":"Non doublon");
Console.WriteLine((v3d == v3dCopyDifferent)?"Doublon":"Non doublon");

Résultat en sortie :

Vector3d { CoordX = 10, CoordY = 20, CoordZ = 30 }
Vector3d { CoordX = 10, CoordY = 20, CoordZ = 30 }
Vector3d { CoordX = 100, CoordY = 20, CoordZ = 30 }
Doublon
Non doublon

Si nous regardons le code IL généré, nous allons pouvoir remarque qu’il se trouve avoir un appel à la méthode clone synthétisée et non accessible. On voit aussi, que la modification de la valeur dans le with et tout simplement un appel à setter de la propriété.

// [19 13 - 19 45]
IL_001e: ldloc.0      // v3d
IL_001f: callvirt     instance class csharpnine_record.Program/Vector3d csharpnine_record.Program/Vector3d::'<Clone>$'()
IL_0024: stloc.2      // v3dCopy

// [20 13 - 20 67]
IL_0025: ldloc.0      // v3d
IL_0026: callvirt     instance class csharpnine_record.Program/Vector3d csharpnine_record.Program/Vector3d::'<Clone>$'()
IL_002b: dup
IL_002c: ldc.i4.s     100 // 0x64
IL_002e: callvirt     instance void csharpnine_record.Program/Vector3d::set_CoordX(int32)
IL_0033: nop
IL_0034: stloc.3      // v3dCopyDifferent

Remarques Importantes sur la copie d’enregistrement

Ce qu’il faut garder à l’esprit c’est que lorsque la copie/clone s’effectue elle n’est que superficielle. Par exemple, si vous avez dans votre objet une propriété qui se trouve être une liste sur des autres enregistrements. Cette liste sera commune à l’original et sa copie.

Toutes modifications sera donc communes pour les deux objets puisqu’ils contiennent la référence vers la même liste.

Voici un exemple

static void Main(string[] args)
{

    Model3d model3d = new Model3d {
        nameOfModel = "Demo01",
        vertices = new List<Vector3d> {
            new Vector3d(0,1,0),
            new Vector3d(1,1,0),
            new Vector3d(1,0,0),
            new Vector3d(0,0,0)
        } 
    };
    
    Model3d model3d_copy = model3d with { };

    // Affichage de l'original et la copie            
    Show();

    // Ajout d'un vertex dans la liste de l'original
    model3d.vertices.Add(new Vector3d(-1,-1,-1));

    // Affichage de l'original et la copie            
    Show();

    Console.WriteLine((model3d == model3d_copy)?"Doublon":"Non doublon");

    void Show() {
        Console.WriteLine("-----------");
        Console.WriteLine(model3d);
        foreach (var item in model3d.vertices) Console.WriteLine(item);
        Console.WriteLine(model3d_copy);
        foreach (var item in model3d_copy.vertices) Console.WriteLine(item);
    }
}

Type de Record

Lors de la déclaration de vos enregistrements, il existe deux manières de les écrire, les déclarer.

Enregistement classique

C’est l’écriture que j’ai utilisé jusqu’ici dans ce billet.

Enregistrement positionnel

Cette forme de déclaration est plus concise que celle précédemment utilisée mais ajoute une méthode synthétisée de déconstruction qui n’est pas présente, sauf si vous la déclarées (méthode Deconstruct) dans l’écriture classique.

Exemple

Je vais reprendre les déclarations des exemples précédents pour les écrires avec les enregistrements positionnels.

public record Model3d (List<Vector3d> Vertices, string NameOfModel);

public record Vector3d (int CoordX, int CoordY, int CoordZ);

public sealed record Vector3dColorized(int CoordX, int CoordY, int CoordZ, Color Colorization) : Vector3d(CoordX, CoordY, CoordZ);

On peut aussi constater que l’on garder l’héritage et même sceller la dernière classe pour empêcher toutes nouvelles dérivations.

L’écriture est plus concise, cela n’empêche pas au développeur d’ajouter ses propres déclarations dans le corps des enregistrements. Dans notre cas, les corps des record sont vides donc on termine directement par le point virgule.

Déconstruction

C’est à partir de C# 7.0 que cette fonctionnalité apparait. La possibilité de regrouper dans une structure de données simples un ensemble d’éléments. Ce sont des éléments de types valeur, publiques et donc mutables.

Je n’entre pas plus dans la fonctionnalités car ce n’est pas le but de ce billet.

Exemple de déconstruction

Reprenons donc notre exemple, réécrit avec les enregistrements positionnels. Et, par exemple, nous voulons simplement le nom du model ainsi que le premier vertex. Nous allons donc écrire le code suivant :

var (demoname, firstvertices) = (model3d.NameOfModel, model3d.Vertices?.First<Vector3d>());
Console.WriteLine("Deconstruction");
Console.WriteLine(demoname);
Console.WriteLine(firstvertices);

et qui aura pour résultat :

Deconstruction

Demo01

Vector3d { CoordX = 0, CoordY = 1, CoordZ = 0 }

Remarquez que la représentation du contenu du vertex est déjà formatté car la méthode ToString est synthétisée dans les records.

Remarque

Ce qu’il faut savoir c’est que l’ensemble des méthodes synthétisées par le compilation peuvent être ré-écrient par vous (overrider) à l’exception de la méthode clone.

Conclusions

Une fonctionnalité qui va enfin nous éviter lors de l’égalité sur des objets complexes de devoir écrire de longue série de tests.

D’avoir une sortie préformattée, une simplification d’écriture avec les positionnels.

Un simple exemple d’utilisation, lorsque l’on veut supprimer des doublons, avec l’équalité de valeurs cela devient quand même plus facile.

….

Frédéric Schmidt

Lead Technical Architect.

Ajouer un commentaire

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Recent Comments

    %d blogueurs aiment cette page :