6. Records, structs et enums
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Dans la leçon 5, deux variables de type classe pouvaient désigner le même objet, et un changement fait par l’une se voyait par l’autre. Beaucoup de choses qu’un programme manipule sont au contraire de simples données : une position sur le manche, une note, la qualité d’un accord. Pour elles, partager un seul objet est rarement ce qu’on veut. C# propose trois autres sortes de types pour cela : la structure (struct), copiée en entier à chaque affectation ; le record, comparé par ses données ; et l’énumération (enum), une liste fixe de choix nommés.
Tous les programmes de cette leçon sont dans code/csharp-beginner ; lance-en un avec dotnet run suivi de son chemin, par exemple examples/l06_copies.cs. check.sh compare leurs sorties, et les erreurs du compilateur pour les extraits refusés, aux fichiers de expected/.
Types valeur et types référence
Section intitulée « Types valeur et types référence »Ci-dessous, FretPosition est une structure et FretMarker une classe. Elles contiennent les deux mêmes nombres, et le programme leur fait subir les mêmes opérations :
// Une structure est un type valeur : l'affecter copie les donnéesFretPosition first = new FretPosition(6, 3); // sol sur la corde de mi graveFretPosition second = first;second.Fret = 5;Console.WriteLine($"first: string {first.StringNumber}, fret {first.Fret}");Console.WriteLine($"second: string {second.StringNumber}, fret {second.Fret}");
// Une classe est un type référence : l'affecter copie la référence (leçon 5)FretMarker marker = new FretMarker(6, 3);FretMarker sameMarker = marker;sameMarker.Fret = 5;Console.WriteLine($"marker: string {marker.StringNumber}, fret {marker.Fret}");
// Une méthode reçoit une copie de la structure, et une copie de la référence à l'objetMoveUpOctave(first);MoveMarkerUpOctave(marker);Console.WriteLine($"after the methods: first at fret {first.Fret}, marker at fret {marker.Fret}");
// Equals compare les champs d'une structure, les références d'une classeConsole.WriteLine($"first.Equals(new FretPosition(6, 3)): {first.Equals(new FretPosition(6, 3))}");Console.WriteLine($"marker.Equals(new FretMarker(6, 17)): {marker.Equals(new FretMarker(6, 17))}");
void MoveUpOctave(FretPosition position) => position.Fret += 12;void MoveMarkerUpOctave(FretMarker target) => target.Fret += 12;
struct FretPosition{ public int StringNumber { get; set; } public int Fret { get; set; }
public FretPosition(int stringNumber, int fret) { StringNumber = stringNumber; Fret = fret; }}
sealed class FretMarker{ public int StringNumber { get; set; } public int Fret { get; set; }
public FretMarker(int stringNumber, int fret) { StringNumber = stringNumber; Fret = fret; }}first: string 6, fret 3second: string 6, fret 5marker: string 6, fret 5after the methods: first at fret 3, marker at fret 17first.Equals(new FretPosition(6, 3)): Truemarker.Equals(new FretMarker(6, 17)): FalseUne structure est un type valeur : la variable contient les données elles-mêmes. second = first copie les deux nombres dans second ; changer second ne touche donc pas first. Une classe est un type référence : la variable contient une référence vers un objet, et sameMarker = marker copie la référence. Il n’y a toujours qu’un seul repère, et les deux variables le voient bouger.
flowchart LR first["first : corde 6, frette 3"] second["second : corde 6, frette 5"] marker["marker"] --> obj["un seul objet FretMarker : corde 6, frette 5"] sameMarker["sameMarker"] --> objLes méthodes suivent la même règle. MoveUpOctave reçoit une copie de first et déplace la copie, qui disparaît au retour de la méthode : first est toujours sur la frette 3. MoveMarkerUpOctave reçoit une copie de la référence ; elle déplace donc l’unique repère, de la frette 5 à la 17. La leçon 4 montrait la même chose avec un int et un tableau : les types numériques de la leçon 2, bool et char sont des structures ; string, les tableaux et List<T> sont des classes.
La comparaison suit aussi cette règle. Le Equals d’une structure compare ses champs un par un (ValueType.Equals) : deux positions distinctes sur la corde 6, frette 3, sont égales. Le Equals d’une classe compare les références : un nouveau repère au même endroit est un autre objet, il n’est donc pas égal.
Deux erreurs avec les structures
Section intitulée « Deux erreurs avec les structures »Une structure que tu déclares n’a pas d’opérateur ==, sauf si tu en écris un. L’extrait refusé compile_fail/l06_struct_equals.cs essaie quand même :
FretPosition first = new FretPosition(6, 3);Console.WriteLine(first == new FretPosition(6, 3));l06_struct_equals.cs(2,19): error CS0019: Operator '==' cannot be applied to operands of type 'FretPosition' and 'FretPosition'La seconde erreur vient avec les listes. shape[0] demande son premier élément à la List<T>, et pour une structure la liste renvoie une copie. Modifier une copie jetée aussitôt ne changerait rien ; le compilateur refuse donc (compile_fail/l06_list_struct_element.cs) :
List<FretPosition> shape = [new FretPosition(6, 3), new FretPosition(5, 5)];shape[0].Fret = 5;l06_list_struct_element.cs(2,1): error CS1612: Cannot modify the return value of 'List<FretPosition>.this[int]' because it is not a variableLis l’élément dans une variable, modifie la variable, puis range-la à sa place avec shape[0] = position;. L’exercice 3 fait la même chose en une seule étape, avec with.
Les structures de cette section ne sont modifiables que pour montrer la copie. Les recommandations de Microsoft pour choisir entre une classe et une structure ne conseillent une structure que pour une petite valeur qui ne change plus une fois créée, et une readonly struct fait respecter cette règle par le compilateur. La section suivante montre la manière courte d’en écrire une.
Les records : comparés par leurs données
Section intitulée « Les records : comparés par leurs données »Un record déclare, en une ligne, un type fait de valeurs nommées :
// Un record : le compilateur écrit le constructeur, les propriétés, ToString et l'égalitéNote c4 = new Note("C", 4);Note middleC = new Note("C", 4);Note c5 = c4 with { Octave = 5 }; // une copie avec une propriété changée
Console.WriteLine(c4);Console.WriteLine(c5);Console.WriteLine($"c4 == middleC: {c4 == middleC}");Console.WriteLine($"same object: {ReferenceEquals(c4, middleC)}");Console.WriteLine($"c4 == c5: {c4 == c5}");
// Une classe avec les mêmes données compare des référencesNoteObject a = new NoteObject("C", 4);NoteObject b = new NoteObject("C", 4);Console.WriteLine($"two NoteObject: {a == b}");
// Un record struct est un type valeur avec les mêmes commoditésPosition g = new Position(6, 3);Position a2 = g with { Fret = 5 };Console.WriteLine($"{g} -> {a2}");Console.WriteLine($"g == new Position(6, 3): {g == new Position(6, 3)}");
record Note(string Name, int Octave);
readonly record struct Position(int StringNumber, int Fret);
sealed class NoteObject{ public string Name { get; } public int Octave { get; }
public NoteObject(string name, int octave) { Name = name; Octave = octave; }}Note { Name = C, Octave = 4 }Note { Name = C, Octave = 5 }c4 == middleC: Truesame object: Falsec4 == c5: Falsetwo NoteObject: FalsePosition { StringNumber = 6, Fret = 3 } -> Position { StringNumber = 6, Fret = 5 }g == new Position(6, 3): Truerecord Note(string Name, int Octave); donne à Note un constructeur à deux paramètres, une propriété publique pour chacun, un ToString qui les affiche, ainsi qu’un Equals et un == qui les comparent. Compare avec les onze lignes de NoteObject, qui n’a toujours pas de ToString et compare des références.
Un record est une classe : c4 et middleC sont deux objets, comme le confirme ReferenceEquals, et pourtant c4 == middleC vaut True, car leurs données sont identiques. L’expression with fait une copie dont certaines propriétés changent : c5 est une nouvelle note, et c4 est toujours à l’octave 4.
readonly record struct Position(...) applique la même idée à un type valeur : copié à l’affectation comme les structures de la section précédente, avec un == et un ToString lisible, et des propriétés qui ne peuvent plus changer une fois la valeur construite.
Les propriétés de Note sont init-only : on peut leur donner une valeur pendant la création du record, jamais après. L’extrait refusé compile_fail/l06_record_init_only.cs essaie c4.Octave = 5; :
l06_record_init_only.cs(2,1): error CS8852: Init-only property or indexer 'Note.Octave' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.Pour obtenir une note à une autre octave, écris c4 with { Octave = 5 }. Dans un simple record struct, sans readonly, les propriétés déclarées entre les parenthèses peuvent changer après la création ; la page sur les records détaille les différences.
Guitar Alchemist déclare ainsi ses positions sur le manche : public readonly record struct PositionLocation(Str Str, Fret Fret), au commit 5c3a52a. Ses Str et Fret sont eux-mêmes des types readonly record struct, plutôt que de simples int.
Les énumérations : un ensemble fixe de noms
Section intitulée « Les énumérations : un ensemble fixe de noms »La qualité d’un accord est un choix parmi quelques-uns : majeur, mineur, diminué… Une enum donne un nom à chaque choix :
// Une enum nomme un ensemble fixe de choix ; chaque nom représente un nombreChordQuality quality = ChordQuality.Minor;Console.WriteLine(quality);Console.WriteLine((int)quality);Console.WriteLine($"A{Suffix(quality)}");
foreach (ChordQuality q in Enum.GetValues<ChordQuality>()){ Console.WriteLine($"{(int)q} {q}: C{Suffix(q)}");}
ChordQuality unset = default; // la valeur 0ChordQuality zero = 0; // le littéral 0 se convertit sans castConsole.WriteLine($"default: {unset}, 0: {zero}");
ChordQuality strange = (ChordQuality)7; // un cast accepte n'importe quel intConsole.WriteLine($"cast from 7: {strange}, defined: {Enum.IsDefined(strange)}");
ChordQuality parsed = Enum.Parse<ChordQuality>("Diminished");Console.WriteLine($"parsed: {parsed} = {(int)parsed}");
string Suffix(ChordQuality q) => q switch{ ChordQuality.Major => "", ChordQuality.Minor => "m", ChordQuality.Diminished => "dim", ChordQuality.Augmented => "aug", _ => "?",};
enum ChordQuality{ Other, Major, Minor, Diminished, Augmented,}Minor2Am0 Other: C?1 Major: C2 Minor: Cm3 Diminished: Cdim4 Augmented: Caugdefault: Other, 0: Othercast from 7: 7, defined: Falseparsed: Diminished = 3Chaque nom représente un int, numéroté à partir de 0 dans l’ordre de la déclaration : Minor vaut 2. Console.WriteLine affiche le nom, et le cast (int) donne le nombre. Enum.GetValues<ChordQuality>() liste les valeurs, triées par nombre, et Enum.Parse retrouve une valeur à partir de son nom écrit en texte. Une expression switch, vue à la leçon 3, est la façon habituelle de donner à chaque choix son propre résultat.
Une énumération est un type valeur, et sa valeur par défaut est 0, le premier nom. Les recommandations de Microsoft pour les énumérations demandent donc un membre valant zéro qui ait du sens comme valeur par défaut. Le ChordQuality de Guitar Alchemist commence par Other pour cette raison ; l’énumération plus petite de cette leçon reprend l’idée.
Nombres et noms ne se mélangent pas librement. Le littéral 0 se convertit tout seul en n’importe quelle énumération, comme le montre zero, mais tout autre nombre demande un cast. L’extrait refusé compile_fail/l06_int_to_enum.cs écrit ChordQuality quality = 2; :
l06_int_to_enum.cs(1,24): error CS0266: Cannot implicitly convert type 'int' to 'ChordQuality'. An explicit conversion exists (are you missing a cast?)Le cast, lui, ne vérifie rien : (ChordQuality)7 est accepté et affiche 7, une valeur sans nom. Enum.IsDefined indique si une valeur a un nom. La référence des énumérations met en garde contre ces deux conversions.
Une expression switch sur une enum a quand même besoin de _
Section intitulée « Une expression switch sur une enum a quand même besoin de _ »Comme une énumération peut contenir des nombres sans nom, une expression switch avec une branche par nom n’est toujours pas complète. examples/l06_enum_switch_warning.cs :
ChordQuality quality = (ChordQuality)7;
// Chaque nom a sa branche, et le compilateur avertit quand même : une enum peut contenir d'autres nombresstring suffix = quality switch{ ChordQuality.Other => "?", ChordQuality.Major => "", ChordQuality.Minor => "m", ChordQuality.Diminished => "dim", ChordQuality.Augmented => "aug",};Console.WriteLine(suffix);l06_enum_switch_warning.cs(4,25): warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(ChordQuality)5' is not covered.Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.Unmatched value was 7.Les lignes de la trace de pile, qui commencent par at, sont omises. L’avertissement est CS8524, un cousin du CS8509 de la leçon 3. Son exemple, (ChordQuality)5, est le premier nombre après le dernier nom, pas le 7 sur lequel le programme échoue. La branche _ => "?" de Suffix, plus haut, couvre les deux.
Exercices
Section intitulée « Exercices »Exercice 1 — prévoir les copies
Section intitulée « Exercice 1 — prévoir les copies »Sans le lancer, prévois ce qu’affiche exercises/l06_ex_predict.cs. Tempo est une structure, Metronome une classe.
Tempo a = new Tempo(120);Tempo b = a;b.Bpm = 90;
Metronome m1 = new Metronome(120);Metronome m2 = m1;m2.Bpm = 90;
Console.WriteLine($"{a.Bpm} {b.Bpm} {m1.Bpm} {m2.Bpm}");
struct Tempo{ public int Bpm { get; set; }
public Tempo(int bpm) => Bpm = bpm;}
sealed class Metronome{ public int Bpm { get; set; }
public Metronome(int bpm) => Bpm = bpm;}Solution
120 90 90 90b = a copie le tempo : a garde 120. m2 = m1 copie la référence : il n’y a qu’un métronome, et il est à 90 par les deux variables.
Exercice 2 — les accords de do majeur
Section intitulée « Exercice 2 — les accords de do majeur »Déclare une énumération ChordQuality avec Other, Major, Minor et Diminished, et un record Chord(string Root, ChordQuality Quality) avec une méthode Symbol() qui renvoie C, Dm ou Bdim. Mets les sept accords de do majeur dans une List<Chord>, affiche leurs symboles sur une ligne, puis vérifie avec Contains si la liste contient un nouveau Chord("A", ChordQuality.Minor), et le même accord rendu majeur avec with. La solution testée est exercises/l06_ex_chords.cs.
Solution
List<Chord> cMajor =[ new Chord("C", ChordQuality.Major), new Chord("D", ChordQuality.Minor), new Chord("E", ChordQuality.Minor), new Chord("F", ChordQuality.Major), new Chord("G", ChordQuality.Major), new Chord("A", ChordQuality.Minor), new Chord("B", ChordQuality.Diminished),];
List<string> symbols = [];foreach (Chord chord in cMajor){ symbols.Add(chord.Symbol());}Console.WriteLine(string.Join(" ", symbols));
Chord am = new Chord("A", ChordQuality.Minor);Chord aMajor = am with { Quality = ChordQuality.Major };Console.WriteLine($"Contains {am.Symbol()}: {cMajor.Contains(am)}");Console.WriteLine($"Contains {aMajor.Symbol()}: {cMajor.Contains(aMajor)}");
record Chord(string Root, ChordQuality Quality){ public string Symbol() => Quality switch { ChordQuality.Major => Root, ChordQuality.Minor => $"{Root}m", ChordQuality.Diminished => $"{Root}dim", _ => $"{Root}?", };}
enum ChordQuality{ Other, Major, Minor, Diminished,}C Dm Em F G Am BdimContains Am: TrueContains A: FalseContains compare avec Equals. L’objet am n’a jamais été mis dans la liste, mais un record compare des données : il retrouve donc le la mineur de la liste. Avec une classe qui compare des références, comme NoteObject, la même recherche répond False. Un record peut avoir des méthodes après sa ligne de déclaration, entre accolades, comme une classe.
Exercice 3 — déplacer une forme d’accord
Section intitulée « Exercice 3 — déplacer une forme d’accord »List<Position> shape = [new Position(6, 3), new Position(5, 5), new Position(4, 5)]; est un power chord G5, avec readonly record struct Position(int StringNumber, int Fret). Monte la forme de deux frettes, en A5, et affiche-la sous la forme 6:5 5:7 4:7. shape[i].Fret += 2 est refusé : les propriétés de Position sont init-only, et le compilateur répond par l’erreur CS8852 de la section sur les records (compile_fail/l06_readonly_position.cs). La solution testée est exercises/l06_ex_move_shape.cs.
Solution
// G5, un power chord : corde 6 frette 3, corde 5 frette 5, corde 4 frette 5List<Position> shape = [new Position(6, 3), new Position(5, 5), new Position(4, 5)];
// shape[i].Fret += 2 est refusé : on remplace chaque élément par une copie déplacéefor (int i = 0; i < shape.Count; i++){ shape[i] = shape[i] with { Fret = shape[i].Fret + 2 };}
List<string> cells = [];foreach (Position p in shape){ cells.Add($"{p.StringNumber}:{p.Fret}");}Console.WriteLine(string.Join(" ", cells));
readonly record struct Position(int StringNumber, int Fret);6:5 5:7 4:7with construit une copie déplacée, et shape[i] = … la range à la place de l’ancien élément. Avec un type readonly, c’est la seule façon de faire : même une variable locale de type Position ne peut pas voir sa Fret changer, et le compilateur répond de nouveau CS8852.
Ce qu’il faut retenir
Section intitulée « Ce qu’il faut retenir »- Une structure est un type valeur : l’affecter, ou la passer à une méthode, copie les données.
- Une classe est un type référence : l’affecter copie la référence, et les deux variables atteignent le même objet.
- Un record compare ses données avec
==etEquals, les affiche avecToStringet fait des copies modifiées avecwith. - Une énumération nomme un ensemble fixe de choix ; chaque nom représente un nombre, et la valeur par défaut est 0.
- Un cast vers une énumération accepte n’importe quel nombre : vérifie avec
Enum.IsDefined, et donne une branche_à une expressionswitch.
La suite, interfaces et héritage, montrera des classes qui partagent un comportement.
- Types valeur, types référence, types structure, valeurs par défaut
- Records, les records dans les fondamentaux de C#, l’expression
with - Comparaisons d’égalité, opérateurs d’égalité,
ValueType.Equals,Object.ReferenceEquals - Types énumération,
Enum.GetValues,Enum.IsDefined,Enum.Parse - Recommandations de conception : choisir entre classe et structure, conception des énumérations
- Avertissement du compilateur CS8524