1. Le problème du transfert de données
Imaginons qu'on a une appli pour une école, et qu'on doit transmettre des infos sur un élève entre différents modules : nom, année de naissance, classe. Comment on fait d'habitude ?
On crée une classe du genre :
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Ça a l'air classique. Mais cette approche a quelques soucis :
- Pour comparer deux élèves, par défaut c'est juste la référence de l'objet qui est comparée. Donc deux élèves avec les mêmes champs mais des objets différents — ne sont pas égaux !
- La classe peut être modifiée après création, ce qui peut causer des bugs (surtout si l'objet est déjà utilisé quelque part) ;
- Beaucoup de code « boilerplate » : constructeurs, méthodes de comparaison, de copie (clones), ToString.
Et comme tu t'en doutes sûrement : C# peut nous éviter tout ça ! Voici — record.
2. C'est quoi un record ?
Un record — c'est un type spécial en C# conçu spécialement pour stocker des données. Un record a deux gros avantages :
- Immutabilité (immutability) : les objets de type record sont immuables par défaut, c'est-à-dire que leurs propriétés sont fixées à la création et ne changent plus (en vrai, les setters sont privés). Même si tu peux déclarer des records mutables, par défaut — c'est immuable.
- Comparaison par valeur : si deux objets record ont les mêmes valeurs dans tous leurs champs, ils sont considérés comme égaux (== et .Equals() fonctionnent différemment !).
En vrai, un record — c'est l'emballage parfait pour transférer des données entre les couches d'une appli (genre, de la BDD au contrôleur, du contrôleur à la vue, etc.).
3. Syntaxe des records
Le moyen le plus simple — la syntaxe positionnelle
Quand on veut juste transmettre un ensemble de valeurs, on déclare le type avec une seule ligne :
public record Student(string Name, int YearOfBirth, string Class);
Qu'est-ce qui se passe sous le capot ? Le compilateur va générer pour nous :
- Des propriétés automatiques en lecture seule (avec un setter privé) ;
- Un constructeur qui prend tous les paramètres ;
- Des méthodes de comparaison et de copie ;
- Un ToString stylé qui formate bien l'affichage !
Utilisation d'un record positionnel
Essayons d'utiliser ce nouveau type dans notre appli scolaire :
var student1 = new Student("Ivan", 2008, "8A");
var student2 = new Student("Maria", 2008, "8B");
L'accès aux propriétés se fait comme d'hab (sauf qu'on ne peut pas les modifier) :
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
Tenter de modifier une propriété après création
student1.Name = "Petr"; // Erreur ! Propriété en lecture seule.
Si tu décommentes cette ligne — le compilateur va râler direct : impossible d'assigner une valeur à une propriété en lecture seule.
Voilà à quoi ressemble le ToString généré automatiquement
Console.WriteLine(student1); // Affichera : Student { Name = Ivan, YearOfBirth = 2008, Class = 8A }
C'est propre et clair même sans formatage manuel !
4. Comparaison des objets record
Petit rappel : si tu crées deux objets différents d'une classe classique avec les mêmes données, ils ne seront quand même pas égaux :
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // Pour une classe : false
Mais si Student est un record, alors l'égalité marche comme tu t'y attends :
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // Pour un record : true !
Donc deux records avec les mêmes champs sont considérés égaux même si ce sont deux objets différents en mémoire.
5. À quoi ça ressemble à l'intérieur
Les étudiants sont souvent surpris de voir tout ce que le compilateur fait pour nous avec les records. Comparons pour voir ce qu'il faudrait écrire à la main pour une classe, et ce que fait un record.
La bonne vieille classe écrite à la main
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //référence à sa propre classe
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
Pas étonnant que les devs deviennent parano — on écrit dix fois la même chose !
Record — une seule ligne
public record Student(string Name, int YearOfBirth, string Class);
6. Record et immutabilité : ce qu'on peut faire ou pas
Les records ont par défaut des propriétés en lecture seule, et ça évite plein de bugs. Mais si tu veux vraiment (genre, tu bosses avec un vieux API), tu peux déclarer des records mutables :
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Maintenant tu peux modifier ces champs, mais tu perds certains avantages (la sécurité, par exemple).
7. Déstructuration d'un record
Comme la syntaxe positionnelle ressemble beaucoup aux tuples, tu peux facilement déstructurer un record :
var student = new Student("Ivan", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // Ivan - 2008, 8A
Le compilateur génère une méthode Deconstruct pour chaque record positionnel, et ça rend la vie plus cool avec LINQ, les switch-patterns et tout le reste.
8. Record — c'est une classe ou un struct ?
Par défaut, un record — c'est un type référence, comme une classe. Donc tout le comportement des types référence (stockage sur le heap, copie de référence, etc.) marche normalement.
Si tu veux un type valeur (value type), C# a aussi ce qu'il faut — tu peux écrire :
public record struct Point(int X, int Y);
Mais pour transférer des données, on utilise presque toujours la forme classique des records, donc type référence. Plus de détails sur record struct — dans les prochaines leçons :P
Comparaison entre class, struct, record
| Type | Immutabilité par défaut | Comparaison par valeur | Déstructuration facile | Auto ToString |
|---|---|---|---|---|
| class | Non | Non (par référence) | Non | Non |
| struct | Non | Oui | Non | Non |
| record | Oui | Oui | Oui | Oui |
9. Particularités et erreurs fréquentes
Beaucoup de débutants se mélangent dans les subtilités des records. Par exemple, certains pensent que modifier un champ d'un record va modifier l'autre (comme avec les classes et la copie de référence). Non ! Aussi, quand tu utilises with, rappelle-toi qu'une copie est toujours créée, l'original ne change pas. Les records sont parfaits pour la logique où ton appli veut garder des données propres et prévisibles.
Oui, si tu déclares des champs avec init au lieu de set, tu peux aussi les assigner seulement à la création ou avec with, mais pas après.
GO TO FULL VERSION