Tutoriels gratuits et ouverts à toutes et à tous · conçus au Lycée Chevrollier — AngersÀ propos

Série « Parcours Rust » · partie 6 / 8

Rust niveau 5 — Traits, génériques et tests

Écrire du code réutilisable, l’organiser en modules et le prouver par des tests : niveau 5 du parcours Rust, avec exercices corrigés et QCM.

Développement✎ B. Jousseaume📅 17 octobre 2026⏱ 7 h📶 Intermédiaire

1. Les traits

Un trait décrit un comportement : une liste de méthodes qu’un type s’engage à fournir.

trait Decrire {
    fn description(&self) -> String;

    // méthode avec une implémentation par défaut
    fn fiche(&self) -> String {
        format!("[fiche] {}", self.description())
    }
}

struct Poste { nom: String, ram_go: u32 }
struct Imprimante { modele: String, couleur: bool }

impl Decrire for Poste {
    fn description(&self) -> String {
        format!("poste {} ({} Go)", self.nom, self.ram_go)
    }
}

impl Decrire for Imprimante {
    fn description(&self) -> String {
        let c = if self.couleur { "couleur" } else { "noir et blanc" };
        format!("imprimante {} {}", self.modele, c)
    }
    fn fiche(&self) -> String {                 // on remplace la version par défaut
        format!("[matériel partagé] {}", self.description())
    }
}

fn main() {
    let p = Poste { nom: String::from("PC-207-01"), ram_go: 16 };
    let i = Imprimante { modele: String::from("LaserPro"), couleur: false };
    println!("{}", p.fiche());
    println!("{}", i.fiche());
}

Les traits standard et derive

TraitApporteObtention
DebugAffichage {:?}#[derive(Debug)]
DisplayAffichage {} et .to_string()À écrire soi-même
Clone, CopyCopie explicite, copie implicite#[derive(Clone, Copy)]
PartialEq, EqComparaison ==#[derive(PartialEq, Eq)]
PartialOrd, OrdComparaisons <, > et tri#[derive(PartialOrd, Ord)]
HashUtilisation comme clé de HashMap#[derive(Hash)]
DefaultValeur par défaut Type::default()#[derive(Default)]
From<T>Conversion Type::from(t) et t.into()À écrire soi-même
use std::fmt;

#[derive(Debug, Clone, PartialEq, Default)]
struct Salle {
    batiment: char,
    numero: u16,
}

impl fmt::Display for Salle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}{:03}", self.batiment, self.numero)
    }
}

impl From<(char, u16)> for Salle {
    fn from((batiment, numero): (char, u16)) -> Self {
        Salle { batiment, numero }
    }
}

fn main() {
    let a = Salle { batiment: 'B', numero: 7 };
    let b: Salle = ('B', 7).into();              // grâce à From
    println!("{a} / {:?} / égales : {}", b, a == b);
    println!("par défaut : {:?}", Salle::default());
}

2. Les génériques

Une fonction générique fonctionne pour plusieurs types. On écrit le type sous forme de paramètre T, et on précise ce que T doit savoir faire : c’est une contrainte de trait.

// T doit pouvoir être comparé (PartialOrd)
fn plus_grand<T: PartialOrd>(valeurs: &[T]) -> Option<&T> {
    let mut max = valeurs.first()?;
    for v in valeurs {
        if v > max {
            max = v;
        }
    }
    Some(max)
}

fn main() {
    println!("{:?}", plus_grand(&[3, 9, 2]));
    println!("{:?}", plus_grand(&[1.5, 0.2]));
    println!("{:?}", plus_grand(&["poire", "abricot", "sureau"]));
    println!("{:?}", plus_grand::<i32>(&[]));
}

Le compilateur génère une version de la fonction pour chaque type utilisé : les génériques ne coûtent rien à l’exécution.

use std::fmt::Display;

// plusieurs contraintes, écrites avec where pour la lisibilité
fn afficher_tous<T>(elements: &[T])
where
    T: Display,
{
    for (i, e) in elements.iter().enumerate() {
        println!("{}. {e}", i + 1);
    }
}

// une structure générique
#[derive(Debug)]
struct Paire<T> {
    a: T,
    b: T,
}

impl<T: PartialOrd + Copy> Paire<T> {
    fn plus_grand(&self) -> T {
        if self.a >= self.b { self.a } else { self.b }
    }
}

fn main() {
    afficher_tous(&["Lina", "Sam"]);
    afficher_tous(&[3.5, 7.25]);
    let p = Paire { a: 4, b: 11 };
    println!("{:?} → {}", p, p.plus_grand());
}

impl Trait et dyn Trait

trait Forme {
    fn aire(&self) -> f64;
    fn nom(&self) -> String;
}

struct Cercle { r: f64 }
struct Carre { c: f64 }

impl Forme for Cercle {
    fn aire(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
    fn nom(&self) -> String { format!("cercle de rayon {}", self.r) }
}
impl Forme for Carre {
    fn aire(&self) -> f64 { self.c * self.c }
    fn nom(&self) -> String { format!("carré de côté {}", self.c) }
}

// impl Forme : « n'importe quel type qui implémente Forme » (un seul type à la fois)
fn decrire(f: &impl Forme) {
    println!("{} : {:.2}", f.nom(), f.aire());
}

fn main() {
    decrire(&Cercle { r: 1.0 });

    // dyn Forme : une collection de types DIFFÉRENTS qui implémentent Forme
    let formes: Vec<Box<dyn Forme>> = vec![
        Box::new(Cercle { r: 2.0 }),
        Box::new(Carre { c: 3.0 }),
    ];
    let total: f64 = formes.iter().map(|f| f.aire()).sum();
    println!("aire totale : {total:.2}");
}
ÉcritureSensUsage
fn f<T: Forme>(x: &T) ou fn f(x: &impl Forme)Un type précis, choisi à la compilationCas général, le plus rapide
Vec<Box<dyn Forme>>Des types différents, choisis à l’exécutionCollections hétérogènes, greffons

3. Fermetures et itérateurs

Une fermeture (closure) est une fonction anonyme, écrite entre barres verticales, qui peut utiliser les variables qui l’entourent.

fn main() {
    let seuil = 10.0;
    let est_admis = |note: f64| note >= seuil;      // capture seuil
    let double = |x| x * 2;
    println!("{} {}", est_admis(12.5), double(21));
}

Un itérateur produit des éléments un par un. On enchaîne des adaptateurs qui transforment le flux, puis un consommateur qui produit le résultat. Rien n’est calculé tant qu’il n’y a pas de consommateur.

fn main() {
    let notes = vec![12.0, 7.5, 15.0, 9.0, 18.5, 10.0];

    let admises: Vec<f64> = notes.iter().copied().filter(|n| *n >= 10.0).collect();
    let bonifiees: Vec<f64> = notes.iter().map(|n| (n + 1.0).min(20.0)).collect();
    let nb_echecs = notes.iter().filter(|n| **n < 10.0).count();
    let somme: f64 = notes.iter().sum();
    let meilleure = notes.iter().copied().fold(f64::MIN, f64::max);
    let tous_positifs = notes.iter().all(|n| *n >= 0.0);
    let un_vingt = notes.iter().any(|n| *n == 20.0);
    let position = notes.iter().position(|n| *n == 15.0);

    println!("{admises:?}\n{bonifiees:?}");
    println!("{nb_echecs} échecs, somme {somme}, meilleure {meilleure}");
    println!("{tous_positifs} {un_vingt} {position:?}");

    let noms = ["Lina", "Sam", "Noé"];
    for (rang, (nom, note)) in noms.iter().zip(notes.iter()).enumerate() {
        println!("{}. {nom} : {note}", rang + 1);
    }
}
Adaptateur ou consommateurEffet
map(|x| …)Transforme chaque élément
filter(|x| …)Garde les éléments qui vérifient la condition
enumerate(), zip(autre)Ajoute l’indice / avance sur deux itérateurs à la fois
take(n), skip(n)Les n premiers / tout sauf les n premiers
collect()Rassemble dans une collection (Vec, String, HashMap…)
sum(), count(), min(), max()Calcule un résultat
any(), all(), find(), position()Recherche, s’arrête dès que possible
fold(init, |acc, x| …)Accumule un résultat quelconque

4. Les durées de vie

Quand une fonction renvoie une référence, le compilateur doit savoir de quel paramètre elle provient, pour vérifier qu’elle ne survivra pas à la donnée. Le plus souvent, il le déduit seul. Sinon, on l’indique avec une durée de vie notée 'a :

fn plus_long<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

// une structure qui contient une référence doit aussi l'annoncer
struct Extrait<'a> {
    texte: &'a str,
}

fn main() {
    let s1 = String::from("administration");
    let resultat;
    {
        let s2 = String::from("réseau");
        resultat = plus_long(&s1, &s2);
        println!("{resultat}");
    }
    // println!("{resultat}");  ← refusé : s2 n'existe plus, resultat pourrait la désigner

    let e = Extrait { texte: &s1[0..5] };
    println!("{}", e.texte);
}

'a ne change pas la durée de vie des données : il décrit une relation (« le résultat vit au plus aussi longtemps que a et b ») que le compilateur vérifie. Si les durées de vie deviennent compliquées dans ton code, c’est souvent le signe qu’une structure devrait posséder ses données (String) plutôt que les emprunter (&str).

5. Organiser le code en modules

mod validation {
    pub struct Identifiant(String);                // la structure est publique…

    impl Identifiant {
        pub fn new(texte: &str) -> Option<Self> {
            let ok = (3..=20).contains(&texte.len())
                && texte.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
            if ok { Some(Identifiant(texte.to_string())) } else { None }
        }
        pub fn valeur(&self) -> &str {
            &self.0                                  // …mais son champ est privé
        }
    }
}

use validation::Identifiant;

fn main() {
    let id = Identifiant::new("ljousset").expect("identifiant valide");
    println!("{}", id.valeur());
    println!("{}", Identifiant::new("Admin!").is_none());
    // let pirate = Identifiant(String::from("Admin!"));  ← refusé : champ privé
}
  • Tout est privé par défaut : pub rend un élément visible hors de son module.
  • Un champ privé force le passage par le constructeur : c’est ainsi que l’on garantit les types valides du niveau 3.
  • Dans un projet, chaque module va dans son fichier : mod validation; dans main.rs charge src/validation.rs.
  • Un projet peut contenir une bibliothèque (src/lib.rs) et un exécutable (src/main.rs) qui l’utilise : c’est l’organisation du niveau 6.

6. Les tests automatisés

Rust intègre les tests au langage. On les place dans un module tests, compilé seulement lors de cargo test.

pub fn moyenne(notes: &[f64]) -> Option<f64> {
    if notes.is_empty() {
        return None;
    }
    Some(notes.iter().sum::<f64>() / notes.len() as f64)
}

pub fn mention(moyenne: f64) -> &'static str {
    match moyenne {
        m if m >= 16.0 => "très bien",
        m if m >= 14.0 => "bien",
        m if m >= 12.0 => "assez bien",
        m if m >= 10.0 => "passable",
        _ => "non admis",
    }
}

#[cfg(test)]
mod tests {
    use super::*;          // accès aux fonctions du fichier

    #[test]
    fn moyenne_simple() {
        assert_eq!(moyenne(&[10.0, 20.0]), Some(15.0));
    }

    #[test]
    fn moyenne_liste_vide() {
        assert_eq!(moyenne(&[]), None);
    }

    #[test]
    fn mentions_aux_limites() {
        assert_eq!(mention(16.0), "très bien");
        assert_eq!(mention(15.99), "bien");
        assert_eq!(mention(10.0), "passable");
        assert_eq!(mention(9.99), "non admis");
    }

    #[test]
    #[should_panic]
    fn indice_invalide() {
        let v: Vec<i32> = Vec::new();
        let _ = v[0];      // ce test réussit si le code panique
    }
}

fn main() {
    println!("{:?} {}", moyenne(&[12.0, 14.0]), mention(13.0));
}
$ cargo test
running 4 tests
test tests::indice_invalide - should panic ... ok
test tests::mentions_aux_limites ... ok
test tests::moyenne_liste_vide ... ok
test tests::moyenne_simple ... ok

test result: ok. 4 passed; 0 failed
MacroVérifie
assert!(condition)Que la condition est vraie
assert_eq!(a, b)Que a == b, et affiche les deux valeurs sinon
assert_ne!(a, b)Que a != b
#[should_panic]Que le test provoque un arrêt

7. Mini-projet : le contrôleur de mots de passe

8. Bilan

Exercices corrigés

Les corrigés de ce niveau contiennent leurs propres tests. Lance cargo test sur ton code et sur le corrigé : si un test du corrigé échoue avec ta version, il a trouvé un cas que tu n’avais pas prévu.

Exercice 5.1 — Un trait pour le parc

Voir le corrigé
use std::fmt;

trait Equipement {
    fn nom(&self) -> String;
    fn consommation_watts(&self) -> u32;

    fn cout_annuel(&self, heures_par_jour: u32, prix_kwh: f64) -> f64 {
        let kwh = self.consommation_watts() as f64 * heures_par_jour as f64 * 365.0 / 1000.0;
        kwh * prix_kwh
    }
}

struct Poste { nom: String }
struct Ecran { pouces: u32 }
struct Switch { ports: u32 }

impl Equipement for Poste {
    fn nom(&self) -> String { format!("poste {}", self.nom) }
    fn consommation_watts(&self) -> u32 { 65 }
}
impl Equipement for Ecran {
    fn nom(&self) -> String { format!("écran {}\"", self.pouces) }
    fn consommation_watts(&self) -> u32 { if self.pouces > 24 { 30 } else { 20 } }
}
impl Equipement for Switch {
    fn nom(&self) -> String { format!("switch {} ports", self.ports) }
    fn consommation_watts(&self) -> u32 { 5 + self.ports }
}

impl fmt::Display for Poste {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Poste<{}>", self.nom)
    }
}

fn inventaire<T: fmt::Display>(liste: &[T]) {
    for e in liste {
        println!("- {e}");
    }
}

fn main() {
    let salle: Vec<Box<dyn Equipement>> = vec![
        Box::new(Poste { nom: String::from("PC-01") }),
        Box::new(Poste { nom: String::from("PC-02") }),
        Box::new(Ecran { pouces: 27 }),
        Box::new(Ecran { pouces: 22 }),
        Box::new(Switch { ports: 24 }),
    ];
    let mut total = 0.0;
    for e in &salle {
        let c = e.cout_annuel(8, 0.25);
        total += c;
        println!("{:<18} {:>4} W {:>8.2} €", e.nom(), e.consommation_watts(), c);
    }
    println!("{:<25} {:>8.2} €", "Total", total);

    inventaire(&[Poste { nom: String::from("PC-01") }, Poste { nom: String::from("PC-02") }]);
}

Explication

La méthode par défaut cout_annuel s’appuie sur consommation_watts, que chaque type fournit : le calcul n’existe qu’en un seul exemplaire. Vec<Box<dyn Equipement>> range dans une même liste des types différents ; inventaire, générique, accepte une liste de n’importe quel type affichable.

Exercice 5.2 — Sans boucle for

Voir le corrigé
fn main() {
    let mots = vec!["serveur", "Pare-feu", "routeur", "", "switch", "VLAN", "proxy"];

    let non_vides = mots.iter().filter(|m| !m.is_empty()).count();

    let mut minuscules: Vec<String> = mots
        .iter()
        .filter(|m| !m.is_empty())
        .map(|m| m.to_lowercase())
        .collect();
    minuscules.sort();

    let plus_long = mots.iter().max_by_key(|m| m.chars().count());

    let total: usize = mots
        .iter()
        .map(|m| m.chars().count())
        .filter(|&n| n > 5)
        .sum();

    let initiales: String = mots
        .iter()
        .filter_map(|m| m.chars().next())
        .map(|c| c.to_ascii_uppercase())
        .collect();

    println!("{non_vides}\n{minuscules:?}\n{plus_long:?}\n{total}\n{initiales}");
}

Explication

filter_map combine un filtre et une transformation : chars().next() renvoie None pour le mot vide, qui est ainsi éliminé. collect() sait fabriquer une String à partir de caractères. max_by_key renvoie le dernier maximum en cas d’égalité : « serveur », « routeur » et « Pare-feu » ont tous 7 ou 8 caractères, vérifie lequel sort.

Exercice 5.3 — Tester une fonction existante

Voir le corrigé
fn sans_accent(c: char) -> char {
    match c {
        'à' | 'â' | 'ä' => 'a',
        'é' | 'è' | 'ê' | 'ë' => 'e',
        'î' | 'ï' => 'i',
        'ô' | 'ö' => 'o',
        'ù' | 'û' | 'ü' => 'u',
        'ç' => 'c',
        'ÿ' => 'y',
        autre => autre,
    }
}

pub fn identifiant(prenom: &str, nom: &str) -> Option<String> {
    let initiale = prenom.trim().chars().next()?;
    let brut: String = std::iter::once(initiale)
        .chain(nom.chars())
        .flat_map(|c| c.to_lowercase())
        .map(sans_accent)
        .filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
        .take(20)
        .collect();
    if brut.is_empty() { None } else { Some(brut) }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test] fn cas_simple() { assert_eq!(identifiant("Lina", "Martin"), Some("lmartin".into())); }
    #[test] fn accents() { assert_eq!(identifiant("Élodie", "Brûlé"), Some("ebrule".into())); }
    #[test] fn nom_compose() { assert_eq!(identifiant("Jean", "Le Goff-Durand"), Some("jlegoffdurand".into())); }
    #[test] fn apostrophe() { assert_eq!(identifiant("Sean", "O'Neil"), Some("soneil".into())); }
    #[test] fn cedille() { assert_eq!(identifiant("François", "Garçon"), Some("fgarcon".into())); }
    #[test] fn troncature() { assert_eq!(identifiant("A", "Abcdefghijklmnopqrstuvwxyz").map(|s| s.len()), Some(20)); }
    #[test] fn prenom_vide() { assert_eq!(identifiant("", "Martin"), None); }
    #[test] fn que_des_symboles() { assert_eq!(identifiant("-", "!!"), None); }
    #[test] fn espaces_autour() { assert_eq!(identifiant("  Sam ", " Noël"), Some("snoel".into())); }
}

fn main() {
    println!("{:?}", identifiant("Élodie", "Le Brûlé"));
}

Explication

std::iter::once(initiale).chain(nom.chars()) construit un flux « initiale puis nom » sans créer de texte intermédiaire. to_lowercase() renvoie un itérateur (certains caractères deviennent plusieurs lettres), d’où flat_map. Le filtre final ne garde que les lettres ASCII minuscules et les chiffres : c’est une liste blanche, plus sûre qu’une liste de caractères interdits.

Pour aller plus loin

La bibliothèque unicode-normalization gère tous les accents de façon exhaustive ; notre match couvre le français courant.

Mini-projet

Voir le corrigé
mod politique {
    pub trait Regle {
        fn verifier(&self, mdp: &str) -> Result<(), String>;
    }

    pub struct LongueurMin(pub usize);
    pub struct Majuscule;
    pub struct Minuscule;
    pub struct Chiffre;
    pub struct Special;
    pub struct PasDansListe(pub Vec<&'static str>);

    impl Regle for LongueurMin {
        fn verifier(&self, mdp: &str) -> Result<(), String> {
            let n = mdp.chars().count();
            if n >= self.0 { Ok(()) } else { Err(format!("au moins {} caractères ({n} actuellement)", self.0)) }
        }
    }
    impl Regle for Majuscule {
        fn verifier(&self, mdp: &str) -> Result<(), String> {
            if mdp.chars().any(|c| c.is_uppercase()) { Ok(()) } else { Err("une majuscule".into()) }
        }
    }
    impl Regle for Minuscule {
        fn verifier(&self, mdp: &str) -> Result<(), String> {
            if mdp.chars().any(|c| c.is_lowercase()) { Ok(()) } else { Err("une minuscule".into()) }
        }
    }
    impl Regle for Chiffre {
        fn verifier(&self, mdp: &str) -> Result<(), String> {
            if mdp.chars().any(|c| c.is_ascii_digit()) { Ok(()) } else { Err("un chiffre".into()) }
        }
    }
    impl Regle for Special {
        fn verifier(&self, mdp: &str) -> Result<(), String> {
            if mdp.chars().any(|c| !c.is_alphanumeric() && !c.is_whitespace()) {
                Ok(())
            } else {
                Err("un caractère spécial".into())
            }
        }
    }
    impl Regle for PasDansListe {
        fn verifier(&self, mdp: &str) -> Result<(), String> {
            let m = mdp.to_lowercase();
            if self.0.iter().any(|interdit| *interdit == m) {
                Err("mot de passe trop courant".into())
            } else {
                Ok(())
            }
        }
    }

    pub struct Politique {
        regles: Vec<Box<dyn Regle>>,
    }

    impl Politique {
        pub fn new() -> Self { Politique { regles: Vec::new() } }

        pub fn ajouter(mut self, r: impl Regle + 'static) -> Self {
            self.regles.push(Box::new(r));
            self
        }

        pub fn standard() -> Self {
            Politique::new()
                .ajouter(LongueurMin(12))
                .ajouter(Majuscule)
                .ajouter(Minuscule)
                .ajouter(Chiffre)
                .ajouter(Special)
                .ajouter(PasDansListe(vec!["azerty", "motdepasse", "123456", "password", "azertyuiop123!"]))
        }

        pub fn controler(&self, mdp: &str) -> Vec<String> {
            self.regles.iter().filter_map(|r| r.verifier(mdp).err()).collect()
        }
    }
}

use politique::Politique;

fn main() {
    let p = Politique::standard();
    for mdp in ["azerty", "Soleil-2026-Angers", "tropcourt1!A"] {
        let erreurs = p.controler(mdp);
        if erreurs.is_empty() {
            println!("{mdp:<22} accepté");
        } else {
            println!("{mdp:<22} refusé : {}", erreurs.join(", "));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::politique::*;

    fn p() -> Politique { Politique::standard() }

    #[test] fn valide() { assert!(p().controler("Soleil-2026-Angers").is_empty()); }
    #[test] fn trop_court() { assert_eq!(p().controler("Ab1!efghijk").len(), 1); }
    #[test] fn sans_majuscule() { assert_eq!(p().controler("soleil-2026-angers"), vec!["une majuscule"]); }
    #[test] fn sans_minuscule() { assert_eq!(p().controler("SOLEIL-2026-ANGERS"), vec!["une minuscule"]); }
    #[test] fn sans_chiffre() { assert_eq!(p().controler("Soleil-deux-Angers"), vec!["un chiffre"]); }
    #[test] fn sans_special() { assert_eq!(p().controler("Soleil2026Angers"), vec!["un caractère spécial"]); }
    #[test] fn courant_casse_ignoree() {
        assert!(p().controler("AZERTYUIOP123!").contains(&"mot de passe trop courant".to_string()));
    }
    #[test] fn vide() { assert!(p().controler("").len() >= 5); }
    #[test] fn espaces_ne_sont_pas_speciaux() { assert!(p().controler("Soleil 2026 Angers").contains(&"un caractère spécial".to_string())); }
    #[test] fn unicode_compte_en_caracteres() { assert!(p().controler("Éléphant-2026").is_empty()); }
    #[test] fn politique_sur_mesure() {
        let courte = Politique::new().ajouter(LongueurMin(4));
        assert!(courte.controler("abcd").is_empty());
    }
}

Explication

Chaque règle est un petit type indépendant ; Politique ne connaît que le trait Regle. Ajouter une règle, c’est ajouter un type, sans toucher au reste : c’est le principe ouvert-fermé. La méthode ajouter prend et renvoie self, ce qui permet d’enchaîner les appels. filter_map(|r| r.verifier(mdp).err()) collecte seulement les erreurs.

Pour aller plus loin

En production, la liste des mots de passe courants contient des centaines de milliers d’entrées, issues de fuites réelles. On la charge depuis un fichier dans un HashSet pour une recherche instantanée.

🧠 Testez vos connaissances

5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.

1. À quoi sert un trait ?

💬 Il joue le rôle d’une interface : un type s’engage à fournir les méthodes du trait.

2. Que signifie fn f<T: PartialOrd>(x: &[T]) ?

💬 La contrainte PartialOrd autorise tout type qui sait se comparer : nombres, textes, et tes propres types s’ils l’implémentent.

3. Quand faut-il Vec<Box<dyn Forme>> plutôt que Vec<Cercle> ?

💬 dyn permet de choisir la méthode à appeler à l’exécution, selon le type réel de chaque élément.

4. Que fait notes.iter().filter(|n| **n >= 10.0).count() ?

💬 filter sélectionne, count consomme en comptant. Le vecteur d’origine n’est pas modifié.

5. Pour une fonction de validation, quels tests écrire en priorité ?

💬 Les failles viennent de ce qu’un contrôle accepte à tort : texte vide, limites, casse, caractères inattendus.

© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.

Pour aller plus loin

Python niveau 1 — Premiers pas

Afficher, calculer, mémoriser dans des variables et dialoguer avec l’utilisateur : niveau 1 du parcours Python, avec exercices corrigés et QCM.

Débutant⏱ 4 à 5 h · 17 octobre 2026