Records
Le scénario
En C#, une classe ordinaire compare par référence, par défaut :
class Point
{
public double X { get; set; }
public double Y { get; set; }
}
var p1 = new Point { X = 1, Y = 2 };
var p2 = new Point { X = 1, Y = 2 };
Console.WriteLine(p1 == p2); // False
p1 et p2 contiennent les mêmes valeurs, et pourtant p1 == p2 renvoie false : ce sont deux emplacements distincts en mémoire, et une classe C# ordinaire ne compare que les adresses, pas le contenu, tant que vous n'avez pas surchargé Equals vous-même (ou utilisé le mot-clé record de C#, une construction séparée que ce module ne suppose pas connue). Le type record de F#, lui, se comporte comme cela par défaut, sans rien avoir à écrire de spécial.
Déclarer un record
type Point = { X: float; Y: float }
Les champs (appelés labels) sont listés entre accolades, séparés par des points-virgules (ou chacun sur sa propre ligne). On construit une instance avec une syntaxe qui ressemble à un initialiseur d'objet C#, mais sans new :
let p1 = { X = 1.0; Y = 2.0 }
let p2 = { X = 1.0; Y = 2.0 }
L'égalité structurelle
C'est ici que le comportement diverge de la classe C# de l'exemple du haut :
p1 = p2
Ceci renvoie true. Un record F# a une égalité structurelle par défaut : = compare la valeur de chaque champ, pas l'adresse mémoire de l'instance. Deux records du même type, avec les mêmes valeurs dans chaque champ, sont égaux — exactement le comportement qu'un record C# offre lui aussi par défaut, mais qui n'est pas celui d'une class C# ordinaire.
Modifier un champ sans muter l'original : with
Les records sont immuables par défaut, dans la continuité directe du module 3 : il n'existe pas de p1.X <- 5.0 pour un champ ordinaire (seul un champ explicitement déclaré mutable dans le type l'autoriserait, ce qui reste rare). Pour obtenir une variante d'un record existant, on utilise une expression de copie-mise à jour avec le mot-clé with, qui crée une nouvelle instance :
let p3 = { p1 with Y = 99.0 }
p3 a X = 1.0 (repris de p1) et Y = 99.0 (la valeur modifiée). p1 lui-même reste inchangé. C'est l'équivalent conceptuel du with expression du C# moderne sur ses propres record, sauf qu'ici il n'y a besoin d'aucun mot-clé record particulier sur le type : c'est le comportement normal de tout type déclaré avec { ... } en F#.
Ce que l'égalité structurelle ne fait pas
Elle ne remplace pas une comparaison par référence si c'est explicitement ce dont vous avez besoin (par exemple pour savoir si deux variables pointent vers exactement le même objet en mémoire) : dans ce cas plus rare, F# fournit l'attribut [<ReferenceEquality>] à poser sur la déclaration du type pour retrouver un comportement d'égalité par référence. Ce n'est pas le comportement par défaut, et ce parcours n'a pas besoin d'aller plus loin sur ce point : retenez simplement que le choix par défaut de F# pour un record est structurel, l'inverse du choix par défaut d'une classe C#.
Exercice — comparer deux records et interpréter le résultat
Dans dotnet fsi, déclarez ce record et ces trois instances :
type Client = { Nom: string; Age: int };;
let a = { Nom = "Dupont"; Age = 42 };;
let b = { Nom = "Dupont"; Age = 42 };;
let c = { Nom = "Martin"; Age = 42 };;
Avant d'exécuter les lignes suivantes, essayez de prédire ce qu'elles vont afficher, puis vérifiez :
a = b;;
a = c;;
Sorties attendues :
val it: bool = true
val it: bool = false
a = b est vrai bien qu'il s'agisse de deux instances distinctes créées séparément : chaque champ a la même valeur dans les deux, donc l'égalité structurelle les considère égales. a = c est faux : le champ Nom diffère, ce qui suffit à rendre l'ensemble différent, même si Age correspond bien de son côté.
Maintenant, créez une variante de a avec with, sans toucher à a lui-même :
let d = { a with Age = 43 };;
a = d;;
a.Age;;
Sorties attendues :
val it: bool = false
val it: int = 42
a = d est faux : with a produit une nouvelle instance dont le champ Age diffère de celui de a. Et a.Age vaut toujours 42 — la preuve que la création de d n'a pas modifié a lui-même, cohérent avec l'immuabilité vue au module 3.
Ce que vous venez de faire
Vous avez déclaré un record F#, constaté que son égalité par défaut compare la valeur de chaque champ plutôt que l'identité de l'instance — à l'opposé du comportement par défaut d'une classe C# ordinaire — et utilisé with pour produire une variante d'un record sans modifier l'original, cohérent avec l'immuabilité par défaut vue au module 3.
C'était le dernier concept nouveau de ce palier Débutant. Le module 7 fait la synthèse des sept modules et vous fait combiner let, |>, match et les records dans un seul exercice, avant d'annoncer ce qui vous attend au palier Intermédiaire.
Vérifiez votre compréhension
Avec type Client = { Nom: string; Age: int }, let a = { Nom = "Dupont"; Age = 42 } et let b = { Nom = "Dupont"; Age = 42 } créés séparément, que renvoie a = b ?
Une classe C# ordinaire (pas un record C#) avec les mêmes champs que Client compare-t-elle deux instances aux mêmes valeurs de la même façon qu'un record F# ?
Après let d = { a with Age = 43 }, que vaut a.Age ?
Envie d'être prévenu des prochains modules ?
L'Académie reste gratuite et en accès libre, sans inscription. Si vous voulez juste être averti par email à la sortie d'un nouveau module, c'est ici — aucune obligation, désinscription en un clic.