Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 421 minIntermédiaire

Structs, enums et match

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • définir une struct, son constructeur new et ses méthodes ;
  • choisir entre &self, &mut self et self ;
  • modéliser des états avec une enum dont les variantes portent des données ;
  • traiter tous les cas avec match, ou un seul avec if let.

4 exercices corrigés

Une struct regroupe des données sous un nom, comme une classe sans héritage ; une enum décrit une valeur qui peut prendre plusieurs formes, chacune avec ses propres données ; et match traite chaque forme possible, sans en oublier aucune. Ces trois outils remplacent en Rust les classes et la hiérarchie d'héritage de C# ou de Java. Ce chapitre construit avec eux le modèle de données de l'outil parc : le poste, son type et son statut.

Définir une struct

rust
#[derive(Debug, Clone, PartialEq)]
struct Poste {
    numero: String,
    marque: String,
    prix_ht: u32, // en centimes
    annee_achat: i32,
}

Une struct déclare des champs typés. Les champs sont des String et non des &str : la struct possède ses données, elle ne les emprunte pas (chapitre 3). La ligne #[derive(…)] demande au compilateur d'écrire du code pour nous :

DérivationCe qu'elle apporte
DebugAfficher la struct avec {:?} ou, sur plusieurs lignes, {:#?}
CloneCopier la struct avec .clone()
PartialEqComparer deux structs avec == et !=, champ par champ

Sans #[derive(Debug)], println!("{poste:?}") ne compile pas. C'est une différence avec C# : rien n'est disponible « gratuitement », chaque capacité se demande. Le chapitre 7 montre que ces dérivations sont des traits.

Ajouter des méthodes avec impl

Les méthodes se déclarent à part, dans un bloc impl :

rust
impl Poste {
    /// Fonction associée : le « constructeur » par convention.
    fn new(numero: &str, marque: &str, prix_ht: u32, annee_achat: i32) -> Poste {
        Poste {
            numero: numero.to_string(),
            marque: marque.to_string(),
            prix_ht,
            annee_achat,
        }
    }

    /// Méthode qui lit le poste : &self.
    fn age(&self, annee_courante: i32) -> i32 {
        annee_courante - self.annee_achat
    }

    /// Méthode qui modifie le poste : &mut self.
    fn appliquer_remise(&mut self, pourcentage: u32) {
        self.prix_ht = self.prix_ht * (100 - pourcentage) / 100;
    }
}

fn main() {
    let mut poste = Poste::new("PC-0001", "Dell", 89900, 2023);
    println!("{} a {} ans", poste.numero, poste.age(2026));

    poste.appliquer_remise(10);
    println!("Prix remisé : {} centimes", poste.prix_ht);

    let jumeau = Poste { numero: String::from("PC-0009"), ..poste.clone() };
    println!("{jumeau:?}");
    println!("{poste:#?}");
    println!("Identiques ? {}", poste == jumeau);
}
text
PC-0001 a 3 ans
Prix remisé : 80910 centimes
Poste { numero: "PC-0009", marque: "Dell", prix_ht: 80910, annee_achat: 2023 }
Poste {
    numero: "PC-0001",
    marque: "Dell",
    prix_ht: 80910,
    annee_achat: 2023,
}
Identiques ? false

Lis ce code ligne par ligne :

  • Pas de constructeur dans le langage. new est une simple fonction associée (sans self), qu'on appelle avec Poste::new(…). Le nom new est une convention, pas un mot-clé.
  • prix_ht, sans : prix_ht : quand le paramètre porte le nom du champ, on l'écrit une seule fois.
  • ..poste.clone() remplit les champs non cités avec ceux d'un autre poste. C'est la syntaxe de mise à jour de struct.
  • La mutabilité porte sur toute la variable : let mut poste rend tous les champs modifiables. Il n'y a pas de champ readonly isolé.

Le premier paramètre d'une méthode dit ce qu'elle fait du poste :

ParamètreLa méthode…Appel possible surExemple
&selflit le postelet poste ou let mut posteage, prix_ttc
&mut selfmodifie le postelet mut poste seulementappliquer_remise, affecter
selfconsomme le poste (il n'est plus utilisable après)toute variable, qui est déplacéeune conversion into_…

Une struct tuple n'a pas de noms de champs : struct Centimes(u64);. Elle sert surtout à donner un type distinct à une valeur, pour que le compilateur refuse de confondre un prix et une quantité. On y accède par .0.

Les enums : une valeur parmi plusieurs formes

Une enum (énumération) liste les formes possibles d'une valeur, ses variantes. Dans sa forme simple, elle ressemble à l'enum de C# :

rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum TypePoste {
    Fixe,
    Portable,
    Serveur,
}

Mais en Rust, chaque variante peut porter des données. Le statut d'un poste en est l'exemple parfait : un poste affecté l'est à quelqu'un, alors qu'un poste en stock n'a pas d'utilisateur.

rust
#[derive(Debug, Clone, PartialEq)]
enum Statut {
    EnStock,
    Affecte(String), // la variante porte le nom de l'utilisateur
    EnReparation,
    Reforme,
}

En C# ou en JavaScript, on aurait écrit un champ statut et un champ utilisateur, et rien n'aurait empêché un poste « en stock » d'avoir un utilisateur, ou un poste « affecté » de ne pas en avoir. Avec l'enum, un état incohérent ne peut pas être représenté : le nom n'existe que dans la variante Affecte.

Une variante peut porter plusieurs valeurs, Affecte(String, i32), ou des champs nommés, EnReparation { depuis: i32, prestataire: String }.

match : traiter tous les cas

match compare une valeur à une liste de motifs (patterns) et exécute la branche du premier qui correspond. C'est un switch bien plus puissant, et c'est une expression qui produit une valeur :

rust
impl TypePoste {
    /// Durée de garantie habituelle, en années.
    fn garantie(&self) -> u32 {
        match self {
            TypePoste::Fixe | TypePoste::Portable => 3,
            TypePoste::Serveur => 5,
        }
    }
}

fn decrire(statut: &Statut) -> String {
    match statut {
        Statut::EnStock => String::from("en stock"),
        Statut::Affecte(nom) => format!("affecté à {nom}"),
        Statut::EnReparation => String::from("en réparation"),
        Statut::Reforme => String::from("réformé"),
    }
}

fn main() {
    let statuts = [
        Statut::Affecte(String::from("Julie Lefèvre")),
        Statut::EnStock,
        Statut::EnReparation,
        Statut::Reforme,
    ];
    for statut in &statuts {
        println!("{}", decrire(statut));
    }
    println!("{:?} : garantie {} ans", TypePoste::Serveur, TypePoste::Serveur.garantie());
    println!("{}", TypePoste::Portable == TypePoste::Fixe);
}
text
affecté à Julie Lefèvre
en stock
en réparation
réformé
Serveur : garantie 5 ans
false
  • Statut::Affecte(nom) extrait la donnée de la variante dans une variable nom, utilisable dans la branche.
  • TypePoste::Fixe | TypePoste::Portable regroupe deux motifs dans une branche.
  • Chaque branche produit une valeur du même type (u32, puis String), qui devient la valeur du match.

La force de match, c'est qu'il est exhaustif : si tu oublies une variante, le programme ne compile pas.

text
error[E0004]: non-exhaustive patterns: `&Statut::EnReparation` not covered
 --> src/main.rs:9:11
  |
9 |     match statut {
  |           ^^^^^^ pattern `&Statut::EnReparation` not covered
  |
note: `Statut` defined here
 --> src/main.rs:1:6
  |
1 | enum Statut {
  |      ^^^^^^
...
4 |     EnReparation,
  |     ------------ not covered

Motifs avancés : intervalles, gardes et joker

Les motifs ne se limitent pas aux enums :

rust
fn priorite(age: i32, garantie: u32) -> &'static str {
    match age {
        i32::MIN..=-1 => "date d'achat incohérente",
        0..=2 => "récent",
        3 | 4 if garantie >= 5 => "encore sous garantie",
        3 | 4 => "à surveiller",
        _ => "à renouveler",
    }
}
MotifCorrespond à
0..=2Un intervalle de valeurs, bornes comprises
3 | 4L'une ou l'autre valeur
3 | 4 if garantie >= 5Les mêmes valeurs, avec une garde : une condition supplémentaire
_Tout le reste (le joker)
Statut::Affecte(nom)Une variante, en extrayant sa donnée

Pour l'appel priorite(4, 5), la troisième branche correspond (4 ans, garantie de 5 ans) : le résultat est « encore sous garantie ». Pour priorite(4, 3), la garde échoue et c'est la branche suivante qui s'applique : « à surveiller ».

if let et let else : un seul cas qui t'intéresse

Quand un seul motif compte, match est verbeux. Deux raccourcis :

rust
let statut = Statut::Affecte(String::from("Karim Morel"));

// if let : un seul motif nous intéresse
if let Statut::Affecte(nom) = &statut {
    println!("Utilisé par {nom}");
}

// let else : on continue seulement si le motif correspond
let Statut::Affecte(nom) = &statut else {
    println!("Poste libre");
    return;
};
println!("Prévenir {nom} avant l'intervention");
text
Utilisé par Karim Morel
Prévenir Karim Morel avant l'intervention
  • if let exécute le bloc seulement si le motif correspond, et peut avoir un else.
  • let … else extrait la donnée ou quitte la fonction (avec return, break ou continue). La variable nom reste ensuite disponible pour toute la suite : c'est idéal pour éviter les if imbriqués.

Option : une valeur qui peut manquer

Rust n'a pas de null. Une valeur qui peut être absente a le type Option<T>, une enum de la bibliothèque standard :

rust
enum Option<T> {
    Some(T), // il y a une valeur, de type T
    None,    // il n'y a rien
}

Some et None s'écrivent sans préfixe, car ils sont toujours disponibles. Un poste peut ainsi avoir un utilisateur, ou pas :

rust
struct Poste {
    numero: String,
    utilisateur: Option<String>,
}

fn main() {
    let postes = [
        Poste { numero: String::from("PC-0001"), utilisateur: Some(String::from("Julie Lefèvre")) },
        Poste { numero: String::from("PC-0007"), utilisateur: None },
    ];
    for poste in &postes {
        match &poste.utilisateur {
            Some(nom) => println!("{} : {nom}", poste.numero),
            None => println!("{} : personne", poste.numero),
        }
    }
}
text
PC-0001 : Julie Lefèvre
PC-0007 : personne

Tu ne peux pas utiliser l'utilisateur sans avoir traité le cas None : le compilateur l'impose. L'erreur « NullReferenceException » de C# ou « Cannot read properties of null » de JavaScript n'existe pas. Le chapitre 5 donne les méthodes pratiques d'Option.

Exercices

Exercice 1 sur 4 La struct Licence Réussi À revoir

Le service informatique suit aussi ses licences logicielles. Définis une struct Licence avec un nom de logiciel, un prix annuel HT en centimes (u32) et un nombre de postes couverts (u32). Ajoute :

  1. une fonction associée new ;
  2. une méthode cout_total(&self) -> u64 (prix annuel × nombre de postes) ;
  3. une méthode ajouter_postes(&mut self, nombre: u32).

Teste avec une suite bureautique à 12 600 centimes par poste, pour 12 postes, puis ajoutes-en 3.

Voir la réponseMasquer la réponse de l'exercice 1
rust
#[derive(Debug)]
struct Licence {
    logiciel: String,
    prix_annuel_ht: u32,
    nb_postes: u32,
}

impl Licence {
    fn new(logiciel: &str, prix_annuel_ht: u32, nb_postes: u32) -> Licence {
        Licence { logiciel: logiciel.to_string(), prix_annuel_ht, nb_postes }
    }

    fn cout_total(&self) -> u64 {
        u64::from(self.prix_annuel_ht) * u64::from(self.nb_postes)
    }

    fn ajouter_postes(&mut self, nombre: u32) {
        self.nb_postes += nombre;
    }
}

fn main() {
    let mut suite = Licence::new("Suite bureautique", 12600, 12);
    println!("{} : {} centimes", suite.logiciel, suite.cout_total());
    suite.ajouter_postes(3);
    println!("{} postes : {} centimes", suite.nb_postes, suite.cout_total());
}
text
Suite bureautique : 151200 centimes
15 postes : 189000 centimes

La conversion en u64 avant la multiplication évite tout dépassement du u32. ajouter_postes prend &mut self, donc la variable doit être déclarée let mut suite.

Tu avais trouvé ?

Exercice 2 sur 4 Ajouter une variante Réussi À revoir

On ajoute la variante EnPret { emprunteur: String, retour: i32 } à l'enum Statut (un poste prêté jusqu'à une année de retour).

  1. Que se passe-t-il à la compilation pour la fonction decrire de ce chapitre ?
  2. Écris la branche à ajouter, qui affiche par exemple « prêté à Chloé Lambert jusqu'en 2027 ».
Voir la réponseMasquer la réponse de l'exercice 2
  1. Le programme ne compile plus : erreur E0004, non-exhaustive patterns: &Statut::EnPret { .. } not covered. Le compilateur montre le match à compléter.
  2. La branche à ajouter dans le match de decrire :
rust
Statut::EnPret { emprunteur, retour } => format!("prêté à {emprunteur} jusqu'en {retour}"),

Les accolades extraient les champs nommés de la variante dans des variables du même nom.

Tu avais trouvé ?

Exercice 3 sur 4 Classer un poste avec match Réussi À revoir

Écris une fonction peut_etre_affecte(statut: &Statut) -> bool qui renvoie true seulement pour un poste en stock. Écris-la d'abord avec un match qui liste toutes les variantes, puis en une ligne avec la macro matches!(valeur, motif). Laquelle préfères-tu si l'on ajoute plus tard une variante ?

Voir la réponseMasquer la réponse de l'exercice 3
rust
fn peut_etre_affecte(statut: &Statut) -> bool {
    match statut {
        Statut::EnStock => true,
        Statut::Affecte(_) | Statut::EnReparation | Statut::Reforme => false,
    }
}

fn peut_etre_affecte_court(statut: &Statut) -> bool {
    matches!(statut, Statut::EnStock)
}

Affecte(_) ignore le nom. Les deux versions donnent le même résultat. La première oblige à décider, à la compilation, si une nouvelle variante (EnPret) peut être affectée ; la seconde renverrait false sans que personne n'y réfléchisse. Pour une règle métier, la version exhaustive est plus sûre.

Tu avais trouvé ?

Exercice 4 sur 4 Lire un if let Réussi À revoir

Que vaut message à la fin de ce code, pour statut = Statut::Reforme puis pour statut = Statut::Affecte(String::from("Inès Bonnet")) ?

rust
let message = if let Statut::Affecte(nom) = &statut {
    format!("à rendre par {nom}")
} else {
    String::from("rien à récupérer")
};
Voir la réponseMasquer la réponse de l'exercice 4

Pour Reforme, le motif ne correspond pas : message vaut « rien à récupérer ». Pour Affecte, il vaut « à rendre par Inès Bonnet ». Comme if, if let est une expression : ses deux branches renvoient une String.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Gérer les erreurs : Option, Result et l'opérateur ?Revoir le sommaire du cours