Exercices Rust
Lire des choses sur Rust, c’est utile. Mais c’est en écrivant du Rust qu’on le retient. Cette page propose 18 petits exercices, classés du plus facile au plus difficile et regroupés par thème.
Pour chacun, lisez l’énoncé, essayez d’abord par vous-même, et n’ouvrez la solution qu’ensuite. C’est tout à fait normal de bloquer ou de voir des erreurs de compilation. Ça fait partie de l’apprentissage, et les messages d’erreur de Rust sont souvent le meilleur indice que vous aurez.
Comment vous entraîner : ouvrez le Rust Playground dans un autre onglet, ou créez un projet avec cargo new practice (voir Débuter avec Rust). Écrivez-y votre réponse, puis comparez-la avec la solution. Si votre réponse est différente mais affiche la même chose, elle est probablement correcte. Il y a presque toujours plusieurs bonnes façons de faire.
Les bases
Section intitulée « Les bases »1. FizzBuzz
Section intitulée « 1. FizzBuzz »Affichez les nombres de 1 à 15. Mais pour les multiples de 3, affichez Fizz ; pour les multiples de 5, Buzz ; et pour les multiples des deux, FizzBuzz.
Indice : utilisez une boucle for sur 1..=15 et l’opérateur % (reste de la division). Testez « multiple des deux » en premier.
Voir la solution
fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } }}Sortie :
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzzL’ordre compte. Si vous testez d’abord n % 3 == 0, alors 15 affiche Fizz et n’atteint jamais la branche FizzBuzz. Un nombre divisible à la fois par 3 et par 5 est divisible par 15.
2. Inverser une chaîne de caractères
Section intitulée « 2. Inverser une chaîne de caractères »Écrivez une fonction reverse(s: &str) -> String qui renvoie le texte à l’envers. "épater" doit devenir "retapé".
Indice : .chars() vous donne les caractères un par un, .rev() inverse un itérateur, et .collect() sait construire une String.
Voir la solution
fn reverse(s: &str) -> String { s.chars().rev().collect()}
fn main() { println!("{}", reverse("épater")); println!("{}", reverse("bonjour")); println!("{}", reverse("Noël"));}Sortie :
retapéruojnoblëoNPourquoi .chars() et pas les octets ? Une String Rust est encodée en UTF-8 : un caractère comme é ou ë occupe deux octets. Inverser les octets le couperait en deux et donnerait un texte invalide. .chars() travaille sur des caractères entiers. Pour en savoir plus, voyez Les chaînes de caractères en Rust.
3. Compter les voyelles
Section intitulée « 3. Compter les voyelles »Écrivez count_vowels(s: &str) -> usize, qui compte les voyelles (a, e, i, o, u) d’une chaîne. Les minuscules comme les majuscules comptent.
Indice : .chars().filter(...).count(). Une chaîne comme "aeiouAEIOU" a une méthode .contains(c) qui permet de tester chaque caractère.
Voir la solution
fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
fn main() { println!("{}", count_vowels("Bonjour, le monde")); println!("{}", count_vowels("Rust est super")); println!("{}", count_vowels("AEIOU et parfois y")); println!("{}", count_vowels("pfff"));}Sortie :
6490filter passe à la closure une référence vers chaque élément (&char) : on écrit donc *c pour obtenir le char lui-même. Les pages Les closures en Rust et Les itérateurs en Rust expliquent ce schéma.
Les voyelles accentuées comme é ou à ne sont pas comptées ici. En bonus, ajoutez-les à la chaîne "aeiouAEIOU" : .contains fonctionne aussi avec elles.
Ownership et borrowing
Section intitulée « Ownership et borrowing »4. Corriger l’erreur de move
Section intitulée « 4. Corriger l’erreur de move »Ce programme ne compile pas. Modifiez uniquement le paramètre de la fonction shout et les deux appels pour qu’il affiche BONJOUR ! deux fois.
fn shout(text: String) { println!("{} !", text.to_uppercase());}
fn main() { let word = String::from("bonjour"); shout(word); shout(word);}error[E0382]: use of moved value: `word` --> src/main.rs:8:11 |6 | let word = String::from("bonjour"); | ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait7 | shout(word); | ---- value moved here8 | shout(word); | ^^^^ value used here after moveLe compilateur dit que word est utilisé après avoir été déplacé (« moved ») : le premier appel a emporté la valeur.
Indice : shout a seulement besoin de lire le texte, pas d’en être propriétaire. Voyez L’ownership en Rust.
Voir la solution
fn shout(text: &str) { println!("{} !", text.to_uppercase());}
fn main() { let word = String::from("bonjour"); shout(&word); shout(&word);}Sortie :
BONJOUR !BONJOUR !Quand une fonction prend une String, l’appeler y déplace (move) la chaîne, et main ne peut plus l’utiliser. Prendre un &str signifie que la fonction ne fait que l’emprunter. Le compilateur suggère aussi word.clone(), qui fonctionne également, mais qui copie toute la chaîne sans raison. Ici, l’emprunt est la meilleure solution.
5. Corriger l’erreur d’emprunt
Section intitulée « 5. Corriger l’erreur d’emprunt »Ce programme ne compile pas non plus. Faites-lui afficher Ana, tout en ajoutant quand même Ben à la liste.
fn main() { let mut names = vec![String::from("Ana")]; let first = &names[0]; names.push(String::from("Ben")); println!("{first}");}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Ben")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereLe message dit qu’on ne peut pas emprunter names de façon mutable, car il est déjà emprunté de façon immuable.
Indice : le problème, c’est l’ordre. first est encore utilisé quand push modifie le vecteur. Voyez Le borrowing en Rust.
Voir la solution
fn main() { let mut names = vec![String::from("Ana")]; names.push(String::from("Ben")); let first = &names[0]; println!("{first}");}Sortie :
AnaPourquoi Rust est-il si strict ici ? push peut avoir besoin de plus de place : le vecteur peut alors déplacer tous ses éléments vers un nouvel emplacement mémoire, plus grand. first pointerait alors vers l’ancien emplacement, qui n’est plus valide. La règle de Rust (plusieurs lecteurs ou un seul écrivain, jamais les deux à la fois) arrête ce bug avant même que votre programme ne tourne. Prendre la référence après le push règle le problème.
6. Le premier mot d’une chaîne
Section intitulée « 6. Le premier mot d’une chaîne »Écrivez first_word(s: &str) -> &str, qui renvoie le premier mot (tout ce qui se trouve avant la première espace). S’il n’y a pas d’espace, renvoyez la chaîne entière. Ne créez pas de nouvelle String : renvoyez une slice de l’original.
Indice : s.find(' ') renvoie un Option<usize> avec la position de la première espace. &s[..i] est une slice qui va du début jusqu’à i.
Voir la solution
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { println!("{:?}", first_word("bonjour le monde")); println!("{:?}", first_word("Rust")); println!("{:?}", first_word(""));}Sortie :
"bonjour""Rust"""Le &str renvoyé pointe vers la même zone mémoire que l’entrée : rien n’est copié. Le compilateur sait que le résultat ne peut pas vivre plus longtemps que l’entrée. C’est l’élision des lifetimes en action : avec une seule référence en entrée et une en sortie, vous n’avez pas à écrire les lifetimes vous-même.
Une version plus courte est s.split_whitespace().next().unwrap_or(""), qui ignore en plus les espaces en trop au début.
Les collections
Section intitulée « Les collections »7. Le plus grand nombre d’un vecteur
Section intitulée « 7. Le plus grand nombre d’un vecteur »Écrivez largest(numbers: &[i32]) -> Option<i32>. Elle renvoie le plus grand nombre, ou None si la liste est vide. Pour cette fois, n’utilisez pas la méthode intégrée .max() : écrivez la boucle vous-même.
Indice : commencez avec le premier élément comme « le plus grand pour l’instant ». numbers.first() renvoie un Option, donc il gère le cas de la liste vide pour vous.
Voir la solution
fn largest(numbers: &[i32]) -> Option<i32> { let mut biggest = *numbers.first()?; for &n in numbers { if n > biggest { biggest = n; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[-5, -2, -9])); println!("{:?}", largest(&[]));}Sortie :
Some(41)Some(-2)Nonenumbers.first()? renvoie None tout de suite si la liste est vide. C’est pour cela que la fonction renvoie un Option : « le plus grand élément de rien » n’a pas de bonne réponse, et Rust vous oblige à le dire. Dans du vrai code, numbers.iter().max() fait le même travail.
Partir de 0 au lieu du premier élément est un bug classique : la liste [-5, -2, -9] donnerait à tort 0.
8. La fréquence des mots
Section intitulée « 8. La fréquence des mots »Comptez combien de fois chaque mot apparaît dans une phrase. Ignorez les majuscules, les minuscules et la ponctuation, et affichez les mots dans l’ordre alphabétique.
Indice : utilisez une BTreeMap<String, usize>. Elle fonctionne comme une HashMap, mais garde ses clés triées. map.entry(word).or_insert(0) vous donne un compteur auquel ajouter 1.
Voir la solution
use std::collections::BTreeMap;
fn main() { let text = "Le chat est sur le tapis. Le chat dort !"; let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() { let clean = word .trim_matches(|c: char| !c.is_alphanumeric()) .to_lowercase(); if clean.is_empty() { continue; } *counts.entry(clean).or_insert(0) += 1; }
for (word, count) in &counts { println!("{word} : {count}"); }}Sortie :
chat : 2dort : 1est : 1le : 3sur : 1tapis : 1Pourquoi BTreeMap et pas HashMap ? Une HashMap vous rend ses éléments dans un ordre imprévisible, qui peut changer d’une exécution à l’autre. Si vous avez besoin d’une sortie triée, utilisez une BTreeMap, ou rassemblez la HashMap dans un Vec et triez-le.
trim_matches retire le . de tapis., et to_lowercase fait de Le et le le même mot. Le ! final, séparé par une espace comme le veut la typographie française, forme un « mot » à lui seul : une fois nettoyé, il est vide, et le continue le saute.
9. Les carrés des nombres pairs
Section intitulée « 9. Les carrés des nombres pairs »À partir de vec![1, 2, 3, 4, 5, 6, 7, 8], construisez un nouveau vecteur avec les carrés des nombres pairs uniquement : [4, 16, 36, 64]. Puis affichez leur somme. Utilisez les méthodes des itérateurs, pas une boucle for.
Indice : enchaînez .iter(), .filter(...), .map(...) et .collect(). Ensuite, .iter().sum() les additionne.
Voir la solution
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares); println!("Somme : {total}");}Sortie :
[4, 16, 36, 64]Somme : 120Lisez la chaîne de haut en bas, comme une recette : prendre les nombres, garder les pairs, mettre chacun au carré, les ranger dans un Vec. Les itérateurs sont paresseux (lazy) : rien ne s’exécute vraiment tant que .collect() ne réclame pas les résultats. Voyez Les itérateurs en Rust.
La gestion des erreurs
Section intitulée « La gestion des erreurs »10. Lire et additionner des nombres
Section intitulée « 10. Lire et additionner des nombres »Écrivez sum_numbers(input: &str) -> Result<i32, ParseIntError>. Elle prend un texte comme "1, 2, 3", convertit chaque nombre et renvoie la somme. Si un morceau n’est pas un nombre, renvoyez l’erreur au lieu de planter.
Indice : découpez sur ',', appliquez .trim() à chaque morceau, puis .parse(). Utilisez l’opérateur ? pour sortir tout de suite en cas d’erreur.
Voir la solution
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> { let mut total = 0; for piece in input.split(',') { let n: i32 = piece.trim().parse()?; total += n; } Ok(total)}
fn main() { for input in ["1, 2, 3", "10,20, 30", "4, cinq, 6", ""] { match sum_numbers(input) { Ok(sum) => println!("{input:?} -> la somme vaut {sum}"), Err(e) => println!("{input:?} -> erreur : {e}"), } }}Sortie :
"1, 2, 3" -> la somme vaut 6"10,20, 30" -> la somme vaut 60"4, cinq, 6" -> erreur : invalid digit found in string"" -> erreur : cannot parse integer from empty stringLes messages d’erreur eux-mêmes restent en anglais, car ils viennent de la bibliothèque standard de Rust : « invalid digit found in string » signifie « chiffre invalide dans la chaîne », et « cannot parse integer from empty string » signifie « impossible de lire un entier dans une chaîne vide ».
? fait deux choses en un seul caractère : si parse() donne Ok(n), vous obtenez n. S’il donne Err(e), la fonction renvoie Err(e) immédiatement. C’est pour cela que la fonction doit elle aussi renvoyer un Result.
Bonus : Result fonctionne avec .sum(), donc cette seule ligne fait la même chose et s’arrête à la première erreur :
input.split(',').map(|p| p.trim().parse::<i32>()).sum()11. Enchaîner des Option
Section intitulée « 11. Enchaîner des Option »Chaque utilisateur a peut-être une adresse e-mail, peut-être pas. Écrivez email_domain(user: &User) -> Option<&str>, qui renvoie la partie située après le @. Renvoyez None s’il n’y a pas d’e-mail, ou si l’e-mail ne contient pas de @. Ensuite, cherchez des utilisateurs par leur nom et affichez leur domaine.
Indice : user.email.as_deref() transforme un Option<String> en Option<&str>. email.split_once('@') renvoie un Option<(&str, &str)>. L’opérateur ? fonctionne aussi avec Option. Voyez Option en Rust.
Voir la solution
struct User { name: String, email: Option<String>,}
fn email_domain(user: &User) -> Option<&str> { let email = user.email.as_deref()?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { let users = vec![ User { name: String::from("Ben"), email: None }, User { name: String::from("Cleo"), email: Some(String::from("cleo-chez-elle")) }, ];
for name in ["Ana", "Ben", "Cleo", "Dan"] { let domain = users .iter() .find(|u| u.name == name) .and_then(email_domain) .unwrap_or("(aucun domaine)"); println!("{name} : {domain}"); }}Sortie :
Ana : example.comBen : (aucun domaine)Cleo : (aucun domaine)Dan : (aucun domaine)Il y a ici trois façons différentes d’« échouer » : Dan n’existe pas, Ben n’a pas d’e-mail, et l’e-mail de Cleo n’a pas de @. Chacune est une étape Option, et ? ou .and_then() s’arrête au premier None. Pas de tests if, pas d’erreurs de pointeur nul. .unwrap_or(...) fournit une valeur par défaut tout à la fin.
12. Un compte bancaire
Section intitulée « 12. Un compte bancaire »Créez une struct Account avec un titulaire et un solde. Donnez-lui deposit(amount) et withdraw(amount). withdraw doit renvoyer un Result<u64, String> : le nouveau solde si l’opération a réussi, ou un message d’erreur s’il n’y a pas assez d’argent. Le solde ne doit jamais passer sous zéro.
Indice : les méthodes qui modifient le compte prennent &mut self. Vérifiez le solde avant de soustraire. Voyez Les structs en Rust et Result en Rust.
Voir la solution
struct Account { owner: String, balance: u64,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }
fn deposit(&mut self, amount: u64) { self.balance += amount; }
fn withdraw(&mut self, amount: u64) -> Result<u64, String> { if amount > self.balance { return Err(format!( "impossible de retirer {amount} : le solde n'est que de {}", self.balance )); } self.balance -= amount; Ok(self.balance) }}
fn main() { let mut account = Account::new("Ana"); account.deposit(100);
for amount in [30, 50, 40] { match account.withdraw(amount) { Ok(left) => println!("Retrait de {amount}, il reste {left}"), Err(e) => println!("Erreur : {e}"), } }
println!("À la fin, {} a {}", account.owner, account.balance);}Sortie :
Retrait de 30, il reste 70Retrait de 50, il reste 20Erreur : impossible de retirer 40 : le solde n'est que de 20À la fin, Ana a 20Utiliser u64 (un nombre non signé) ajoute un joli filet de sécurité : ce type ne peut tout simplement pas contenir un solde négatif. Si vous oubliiez la vérification, self.balance -= amount déclencherait une panic avec le message « attempt to subtract with overflow » (tentative de soustraction avec dépassement) en mode debug, au lieu de passer en négatif sans rien dire.
Traits et génériques
Section intitulée « Traits et génériques »13. Un trait Shape
Section intitulée « 13. Un trait Shape »Définissez un trait Shape avec deux méthodes : area(&self) -> f64 et name(&self) -> String. Implémentez-le pour un Circle (avec un rayon) et un Rectangle (avec une largeur et une hauteur). Mettez les deux dans un même Vec et affichez le nom et l’aire de chacun avec 2 décimales.
Indice : pour ranger des types différents dans un seul Vec, utilisez Vec<Box<dyn Shape>>. Voyez Les traits en Rust et Les trait objects (dyn) en Rust. {:.2} affiche 2 décimales.
Voir la solution
trait Shape { fn area(&self) -> f64; fn name(&self) -> String;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> String { format!("Cercle (r = {})", self.radius) }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> String { format!("Rectangle ({} x {})", self.width, self.height) }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 3.0, height: 4.0 }), Box::new(Circle { radius: 2.5 }), ];
let mut total = 0.0; for shape in &shapes { println!("{} : {:.2}", shape.name(), shape.area()); total += shape.area(); } println!("Aire totale : {total:.2}");}Sortie :
Cercle (r = 1) : 3.14Rectangle (3 x 4) : 12.00Cercle (r = 2.5) : 19.63Aire totale : 34.78La boucle ne sait pas quelle forme elle manipule, et ça lui est égal. Elle sait seulement que chaque élément « est un Shape », donc elle peut appeler area() et name(). Ajouter un Triangle plus tard, c’est écrire un nouveau impl, et la boucle ne change pas du tout.
Remarquez que Rust affiche les nombres décimaux avec un point (3.14), pas avec une virgule : c’est la notation de tous les langages de programmation.
14. Un largest générique
Section intitulée « 14. Un largest générique »Reprenez l’exercice 7, mais faites-le fonctionner pour n’importe quel type comparable : entiers, nombres décimaux, caractères et chaînes. La signature est largest<T: PartialOrd>(items: &[T]) -> Option<&T>.
Indice : T: PartialOrd veut dire « T accepte < et > ». Renvoyez une référence (&T) pour ne pas avoir à copier ou cloner les éléments. Voyez Les génériques en Rust.
Voir la solution
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> { let mut biggest = items.first()?; for item in items { if item > biggest { biggest = item; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[2.5, -1.0, 9.75])); println!("{:?}", largest(&['r', 'u', 's', 't'])); println!("{:?}", largest(&["poire", "abricot", "tomate"]));
let empty: Vec<i32> = Vec::new(); println!("{:?}", largest(&empty));}Sortie :
Some(41)Some(9.75)Some('u')Some("tomate")NoneLe corps est presque identique à celui de l’exercice 7. Le seul vrai changement, c’est la partie <T: PartialOrd>. Sans cette contrainte, le compilateur refuse item > biggest, car tous les types ne sont pas comparables. Les chaînes se comparent dans l’ordre alphabétique, c’est pourquoi "tomate" l’emporte.
Remarquez le Vec<i32> sur le vecteur vide : sans aucun élément, Rust ne peut pas deviner ce qu’est T, donc vous devez le lui dire.
Ensembles, tri, patterns et itérateurs
Section intitulée « Ensembles, tri, patterns et itérateurs »15. Trouver les doublons
Section intitulée « 15. Trouver les doublons »Une liste d’invités contient des noms écrits plusieurs fois. Écrivez find_repeats(words: &[&str]) -> Vec<String>, qui renvoie chaque nom présent plus d’une fois, une seule fois chacun, dans l’ordre alphabétique. Affichez aussi le nombre d’invités différents.
Indice : un HashSet se souvient de ce que vous avez déjà vu. Sa méthode insert renvoie false quand la valeur était déjà présente. Un HashSet n’a pas d’ordre fixe : triez donc le résultat à la fin.
Voir la solution
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> { let mut seen = HashSet::new(); let mut repeats = HashSet::new();
for &word in words { // insert renvoie false si le mot était déjà dans l'ensemble if !seen.insert(word) { repeats.insert(word); } }
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect(); result.sort(); result}
fn main() { let guests = ["ana", "ben", "cleo", "ben", "dan", "ana", "ben"]; println!("Doublons : {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect(); println!("Invités différents : {}", unique.len());
println!("Doublons : {:?}", find_repeats(&["x", "y", "z"]));}Sortie :
Doublons : ["ana", "ben"]Invités différents : 4Doublons : []Pourquoi deux ensembles ? seen contient tous les noms vus jusqu’ici. repeats contient les noms rencontrés une deuxième fois. "ben" apparaît trois fois, mais comme repeats est un ensemble, il n’y est stocké qu’une fois.
Rassembler les valeurs dans un HashSet est aussi le moyen le plus rapide de compter les valeurs uniques : les doublons disparaissent tout simplement. Sans la ligne result.sort(), l’ordre des doublons pourrait changer d’une exécution à l’autre.
16. Trier des mots par longueur
Section intitulée « 16. Trier des mots par longueur »Triez une liste de mots du plus court au plus long. Quand deux mots ont la même longueur, rangez-les de A à Z, sans tenir compte des majuscules et des minuscules. Puis comparez votre résultat avec un simple .sort().
Indice : sort_by prend une closure qui compare deux éléments et renvoie un Ordering. .then_with(...) ajoute une seconde comparaison qui ne compte qu’en cas d’égalité. Voyez Trier et rechercher en Rust.
Voir la solution
fn main() { let mut words = vec!["poire", "Figue", "banane", "pomme", "kiwi", "Cerise", "datte"];
let mut plain = words.clone(); plain.sort(); println!("Tri simple : {plain:?}");
words.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| a.to_lowercase().cmp(&b.to_lowercase())) }); println!("Par longueur : {words:?}");}Sortie :
Tri simple : ["Cerise", "Figue", "banane", "datte", "kiwi", "poire", "pomme"]Par longueur : ["kiwi", "datte", "Figue", "poire", "pomme", "banane", "Cerise"]Regardez le tri simple : "Cerise" et "Figue" arrivent en premier. Les chaînes se comparent selon les codes de leurs caractères, et toutes les majuscules passent avant toutes les minuscules. C’est pourquoi on compare les versions to_lowercase() pour départager les égalités.
Nous avons choisi des mots sans accents exprès : .len() compte des octets, pas des lettres, et une lettre accentuée comme é occupe deux octets. Avec "pêche", .len() vaudrait 6 et non 5. Pour compter les lettres, utilisez .chars().count().
Une façon plus courte d’écrire la même règle est words.sort_by_key(|w| (w.len(), w.to_lowercase())). Les tuples se comparent champ par champ : la longueur décide d’abord, et le mot en minuscules départage les égalités.
17. Frais de port avec des gardes de match
Section intitulée « 17. Frais de port avec des gardes de match »Écrivez shipping_cost(p: Package) -> String pour cet enum, avec un seul match :
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}Les règles : le retrait sur place est gratuit. Les lettres jusqu’à 20 g coûtent 1 €, les lettres plus lourdes 3 €. Les colis de plus de 30 kg ne peuvent pas du tout être expédiés. Les colis express coûtent 10 € plus 2 € par kg. Les autres colis coûtent 5 € plus 1 € par kg.
Indice : une branche de match peut avoir un if après le pattern, qu’on appelle une garde (guard) : Package::Letter { grams } if grams <= 20 => .... Les branches sont testées de haut en bas, donc placez la règle « trop lourd » avant la règle express. Voyez Le pattern matching en Rust.
Voir la solution
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}
fn shipping_cost(p: Package) -> String { match p { Package::Pickup => String::from("gratuit (retrait sur place)"), Package::Letter { grams } if grams <= 20 => String::from("1 € (lettre standard)"), Package::Letter { grams } => format!("3 € (grande lettre, {grams} g)"), Package::Parcel { kg, .. } if kg > 30 => format!("trop lourd pour être expédié ({kg} kg)"), Package::Parcel { kg, express: true } => format!("{} € (express)", 10 + kg * 2), Package::Parcel { kg, express: false } => format!("{} € (colis standard)", 5 + kg), }}
fn main() { let packages = vec![ Package::Letter { grams: 15 }, Package::Letter { grams: 80 }, Package::Parcel { kg: 4, express: false }, Package::Parcel { kg: 4, express: true }, Package::Parcel { kg: 35, express: true }, Package::Pickup, ];
for p in packages { println!("{}", shipping_cost(p)); }}Sortie :
1 € (lettre standard)3 € (grande lettre, 80 g)9 € (colis standard)18 € (express)trop lourd pour être expédié (35 kg)gratuit (retrait sur place)Le colis express de 35 kg est le cas intéressant. Il correspond à deux branches, mais seule la première l’emporte. Si vous placiez la branche express au-dessus de la branche « trop lourd », elle facturerait joyeusement 80 € pour un colis impossible à expédier.
Remarquez que la dernière branche indique express: false au lieu de ... Ce n’est pas obligatoire, mais la branche dit ainsi exactement ce qu’elle traite. Le compilateur vérifie quand même que tous les cas sont couverts. Les gardes ne comptent pas dans cette vérification : c’est pourquoi la branche Package::Letter { grams } sans garde doit être présente.
18. Nettoyer un rapport de stock
Section intitulée « 18. Nettoyer un rapport de stock »Le rapport d’un entrepôt contient un article par ligne, comme "pommes 12". Certaines lignes sont abîmées : vides, ou avec une quantité qui n’est pas un nombre. Avec les méthodes des itérateurs (sans boucle for), gardez uniquement les lignes qui se lisent correctement et dont la quantité est supérieure à zéro. Affichez-les sous la forme nom : quantité, séparées par des virgules, puis affichez le total.
Indice : .lines() découpe le texte en lignes. filter_map ne garde que les valeurs Some renvoyées par sa closure : il peut donc filtrer et convertir en une seule étape. .ok() transforme un Result en Option. Voyez Les itérateurs en Rust.
Voir la solution
fn main() { let report = "pommes 12\nbananes x\npoires 0\n\ncerises 40\nprunes 7";
let in_stock: Vec<(&str, u32)> = report .lines() .filter_map(|line| line.split_once(' ')) .filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n))) .filter(|&(_, n)| n > 0) .collect();
let list: Vec<String> = in_stock .iter() .map(|(name, n)| format!("{name} : {n}")) .collect(); let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", ")); println!("Total des articles : {total}");}Sortie :
pommes : 12, cerises : 40, prunes : 7Total des articles : 59Suivez une ligne à la fois. La ligne vide ne contient pas d’espace : split_once donne None, et le premier filter_map l’écarte. "bananes x" se découpe bien, mais "x" n’est pas un nombre, donc le second filter_map l’écarte. "poires 0" passe les deux étapes, puis filter la retire parce que la quantité vaut zéro.
On fait un seul collect dans un Vec, puis on le parcourt deux fois : une fois pour construire le texte, une fois pour le total. Un itérateur ne peut servir qu’une seule fois, donc garder les données nettoyées est plus simple que de tout relire deux fois.
Et maintenant ?
Section intitulée « Et maintenant ? »Si vous avez terminé les 18 exercices, bravo. C’est un vrai progrès. 🦀
- Construisez un programme complet : essayez les projets jeu de devinettes, compteur de mots ou liste de tâches en ligne de commande.
- Vous préparez un entretien d’embauche ? Lisez les questions d’entretien Rust.
- Envie de vous entraîner encore plus ? Les liens ci-dessous proposent plein d’exercices supplémentaires.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Rustlings (en anglais) : la série officielle de petits programmes cassés à réparer un par un dans votre terminal. Une excellente suite à cette page.
- Le parcours Rust d’Exercism (en anglais) : une centaine d’exercices avec des tests, et des retours gratuits de mentors humains sur vos solutions.
- Rust by Example (en anglais) : de courts exemples exécutables pour chaque sujet. Pratique quand vous bloquez sur la syntaxe d’un exercice.
- Le trait
Iterator— documentation de la std (en anglais) : toutes les méthodes des itérateurs (filter_map,fold,zipet bien d’autres), chacune avec un petit exemple. HashSet— documentation de la std (en anglais) : toutes les méthodes des ensembles, dontunion,intersectionetdifference.