1. Pourquoi on avait besoin des record ?
En C# (et .NET en général), les blocs de base étaient longtemps les classes (class) et les structures (struct). Mais chacun a ses limites pour certains cas. Les classes — c’est des types référence, mutables, et la comparaison se fait par référence (sauf exceptions rares). Les structs — c’est des types valeur (copiés à la transmission), comparés par défaut octet par octet et souvent mutables (jusqu’à l’apparition des readonly struct).
Mais si tu voulais juste une façon simple et naturelle de stocker des données, facile à comparer par valeur, à cloner vite fait et sans te prendre la tête sur qui a modifié ton objet — fallait bricoler des trucs ou utiliser des libs comme ValueTuple ou même System.Tuple. Mais c’est jamais aussi clean et expressif qu’on voudrait.
C’est pour ça qu’en C# 9, ils ont sorti record — un type de données qui combine déclaration courte, sécurité de l’immutabilité et comportement orienté comparaison par valeur.
2. Les quatre types sur une seule page
| class | struct | record | record struct | |
|---|---|---|---|---|
| Catégorie | Type référence | Type valeur | Type référence | Type valeur |
| Mutabilité | Par défaut — oui | Par défaut — oui | Par défaut — non (init) | Par défaut — non (init) |
| Comparaison | Par référence (==) | Par valeur (champs) | Par valeur (fields/properties) | Par valeur (fields) |
| Clonage | Manuellement seulement | Manuellement seulement | Support intégré (with) | Support intégré (with) |
| Héritage | Oui | Non | Oui | Non |
| Immutabilité | À implémenter | À implémenter | Super simple à faire | Super simple à faire |
| Syntaxe | La plus longue | Court | La plus courte (positionnel) | Assez court |
| Utilisation en collections | Par référence | Copies | Par référence | Copies |
Schéma visuel
+----------------+ +----------------+ +--------------------+
| class | | struct | | record |
+----------------+ +----------------+ +--------------------+
| Reference Type | | Value Type | | Reference Type |
| Mutable | | Mutable | | Immutable (init) |
| == : Reference | | == : By Fields | | == : By Value |
+----------------+ +----------------+ +--------------------+
3. L’essence des différences entre record, class et struct
Comportement en mémoire : référence ou valeur ?
- class et record — ce sont des types référence. Quand tu les passes à une fonction, c’est la référence à l’objet qui est copiée.
- struct et record struct — types valeur. Toujours copiés octet par octet (sauf si tu les passes explicitement par référence).
class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);
void Demo()
{
var pc = new PointClass { X = 1, Y = 2 };
var ps = new PointStruct { X = 1, Y = 2 };
var pr = new PointRecord(1, 2);
var prs = new PointRecordStruct(1, 2);
ChangeY(pc); // pc.Y va changer !
ChangeY(ps); // ps.Y ne changera pas — copie !
ChangeY(pr); // pr.Y va changer !
ChangeY(prs); // prs.Y ne changera pas — copie !
}
void ChangeY(dynamic p) { p.Y = 99; }
Si tu trouves que dynamic ici c’est de la magie — ouais, c’est juste pour l’exemple, pour que tu voies qu’avec les structs la valeur ne change pas, alors qu’avec une class ou un record (référence) — ça change.
Comparaison : comment savoir si deux objets sont égaux ?
- class : comparé par référence (== — true seulement si c’est le même objet en mémoire), sauf si tu overrides Equals.
- struct : comparé par valeur de tous les champs (par défaut).
- record : comparé par valeur de tous les champs/propriétés définis dans le constructeur principal.
class Foo { public int A; public int B; }
record Bar(int A, int B);
var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);
Console.WriteLine(foo1 == foo2); // False ! Objets différents
Console.WriteLine(bar1 == bar2); // True ! Valeurs identiques
Fun fact :
Avec record struct c’est encore plus cool : la comparaison est "par valeur", comme un struct classique, mais avec la syntaxe et les features des records.
4. Immutabilité : qui garantit quoi ?
Comparons la sécurité des objets :
- class : Par défaut, facilement modifiable, sauf si tu mets tous les champs en readonly.
- struct : Pareil, mais tu peux déclarer readonly struct, alors ni les champs ni les propriétés ne peuvent changer.
- record : D’habitude, les champs sont déclarés avec le modificateur init, donc tu peux les définir que dans le constructeur ou l’initialiseur d’objet (with). Pratique pour transmettre des données en toute sécurité.
- record struct : Pareil, tu peux faire readonly record struct et avoir l’immutabilité des types valeur avec les bonus des records.
record Person(string Name, int Age);
var p1 = new Person("Alexey", 23);
// p1.Age = 24; // Erreur ! Seulement init
var p2 = p1 with { Age = 24 }; // Ça marche ! Crée une copie avec de nouvelles données
Quand tu bosses sur un gros projet avec des dizaines d’entités, record t’évite plein de bugs du genre "quelqu’un a changé un champ quelque part — et tout a pété".
5. Syntaxe : à quoi ressemblent les déclarations et comment ne pas se perdre
// class
public class Product
{
public int Id { get; init; }
public string Name { get; init; }
}
// struct
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// record
public record Product(int Id, string Name);
// record struct
public record struct Point(int X, int Y);
Comme tu vois, la déclaration la plus courte c’est pour le type record. Tu as le constructeur, le destructeur, l’égalité, ToString() et plein d’autres trucs "out of the box".
| Syntaxe | Ce que tu as gratos | Héritage possible ? |
|---|---|---|
| class | rien (ou presque) | Oui |
| struct | rien (ou presque) | Non |
| record | ToString, Equals, Deconstruct, with | Oui |
| record struct | ToString, Equals, Deconstruct, with | Non |
6. Clonage et opérateur with
Seuls record et record struct ont l’opérateur spécial with, qui te permet de copier un objet en changeant juste certaines propriétés, sans prise de tête.
record User(string Name, int Age);
var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29
Avec une classe (class), tu dois écrire la méthode de copie à la main, et si tu oublies de copier un champ — bonjour les bugs.
7. Héritage : qui hérite de qui
- class : Supporte l’héritage classique (hiérarchies de classes, méthodes virtuelles, abstractions, etc.).
- struct : Ne supporte pas (peut juste implémenter des interfaces).
- record : Supporte l’héritage, mais avec quelques limites (par exemple, héritage possible seulement entre records, pas avec class).
- record struct : Ne supporte pas, comme les structs classiques.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Ok !
class Vehicle { }
class Car : Vehicle { } // Ok !
// struct ne peut pas hériter d’un struct
Plus sur l’héritage au prochain niveau :P
8. Où utiliser quel type en pratique
- class : Gros objets avec beaucoup de logique, longue durée de vie, état mutable, hiérarchie (genre logique métier, composants UI).
- struct : Petits objets-valeurs, où la copie rapide est importante, pas de GC et overhead minimal (genre coordonnées, couleurs, montants — tout ce qui se clone vite et sans douleur).
- record : DTO (Data Transfer Object), objets-paramètres, paramètres de config, états immuables, résultats de calculs, qu’on veut comparer facilement par contenu.
- record struct : Types valeur où tu veux de l’immutabilité et le comportement des records, mais sans allocations inutiles sur le heap.
9. Erreurs classiques et pièges
Parfois, les devs pensent que record c’est juste "un remplaçant de class". Faux ! Si tu fais un objet qui doit changer d’état au cours de sa vie — utilise class.
Si tu veux comparer les objets par référence (genre dans un pattern singleton ou quand le cycle de vie est critique) — utilise class.
Si tu fais un objet-valeur qui doit se comporter comme un nombre ou un point sur une grille — struct ou record struct.
Si tu bosses avec des objets immuables, faciles à comparer, souvent transmis entre couches, stockés en collections, logués et sérialisés — choisis record.
Pense aussi à ça : si tu déclares un record-class avec des propriétés en lecture seule, mais que tu oublies les objets imbriqués — les champs imbriqués peuvent quand même être modifiés s’ils sont eux-mêmes mutables.
record Student(string Name, List<int> Grades);
var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };
s2.Grades.Add(2); // Les deux objets sont "maudits" par le 2 ! s1.Grades == s2.Grades
GO TO FULL VERSION