Exercices introductifs
Le but de ces petits exercices est d'écrire des programmes simples avec une connaissance minimale du Rust. Faites-les dans l'ordre qui vous amuse. N'hésitez pas à refaire plusieurs fois le même exercice en variant le type de structures de contrôle que vous utilisez par exemple.
Année bissextile
Écrire un programme déterminant si une année est bissextile. Pour savoir si une année est bissextile, il faut que:
- l'année soit divisible par 4,
- sauf si elle est divisible par 100,
- sauf si elle est aussi divisible par 400!
Ainsi, l'année 2001 n'est pas bissextile. En revanche l'année 2000 l'est. Contrairement à l'année 1900 qui ne l'est pas.
- Objectif :
if/else, opérateur%, booléens (bases du Rust). - Signature suggérée :
fn est_bissextile(annee: u32) -> bool. - Cas de test :
est_bissextile(2000)->true,est_bissextile(1900)->false,est_bissextile(2001)->false,est_bissextile(2024)->true.
Nombre d'Armstrong
Un nombre d'Armstrong est un nombre qui est la somme de ses propres chiffres, chacun élevé à la puissance du nombre de chiffres.
Par exemple :
- \(9\) est un nombre d'Armstrong, car \(9 = 9^1 = 9\),
- \(10\) n'est pas un nombre d'Armstrong, car \(10 \neq 1^2 + 0^2 = 1\),
- \(153\) est un nombre d'Armstrong, car : \(153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153\),
- \(154\) n'est pas un nombre d'Armstrong, car : \(154 \neq 1^3 + 5^3 + 4^3 = 1 + 125 + 64 = 190\)
Écrivez un programme qui détermine si un nombre est un nombre d'Armstrong.
- Objectif : décomposition d'un nombre en chiffres (
% 10,/ 10), boucle,u32::pow(bases du Rust). - Signature suggérée :
fn est_armstrong(n: u32) -> bool. - Cas de test :
est_armstrong(9)->true,est_armstrong(10)->false,est_armstrong(153)->true,est_armstrong(154)->false,est_armstrong(9474)->true.
Nombres heureux
Un nombre heureux est un entier naturel non nul, qui lorsqu'on calcule la somme des carrés de ses chiffres puis la somme des carrés des chiffres du nombre obtenu et ainsi de suite, on aboutit au nombre 1. Un nombre est malheureux lorsque ce n'est pas le cas.
Ainsi, le nombre 7 est heureux, car:
$$ 7^2=49, $$ $$ 4^2+9^2=97, $$ $$ 9^2+7^2=130, $$ $$ 1^2+3^2+0^2=10, $$ $$ 1^2+0^2=1. $$
D'autres nombres heureux sont 1, 10, 13, 19 par exemple. Il faut faire attention car certains nombres rencontrés dans la suite mènent à une suite périodique: 4, 16, 20, 37, 42, 58, 89, 145 et on ne s'arrête jamais.
Écrivez un programme qui détermine si un nombre est heureux ou malheureux.
- Objectif : boucle avec condition d'arrêt, détection de cycle (mémoriser les valeurs déjà vues, ou s'arrêter dès qu'on atteint
4), fonctions (bases du Rust). - Signature suggérée :
fn est_heureux(n: u32) -> bool. - Cas de test :
est_heureux(7)->true,est_heureux(1)->true,est_heureux(19)->true,est_heureux(2)->false,est_heureux(4)->false.
Différence de nombres
Écrivez un programme qui calcule la différence entre le carré de la somme et la somme des carrés des $N$ premiers nombres naturels.
Le carré de la somme des dix premiers entiers naturels est $$ (1 + 2 + ... + 10)^2 = 55^2 = 3025. $$
La somme des carrés des dix premiers nombres naturels est $$ 1^2 + 2^2 + ... + 10^2 = 385. $$
Donc, la différence entre le carré de la somme des dix premiers nombres naturels et la somme des carrés des dix premiers nombres naturels est $$ 3025 - 385 = 2640. $$
- Objectif : boucle
foret accumulateurs, ou itérateurs ((1..=n).sum(),(1..=n).map(|i| i * i).sum()). - Signature suggérée :
fn difference(n: u32) -> u64. - Cas de test :
difference(10)->2640,difference(1)->0,difference(100)->25164150.
Nombre premier
Écrire un programme qui détermine si un nombre est premier. Un nombre est premier s'il n'a que deux diviseurs distincts.
- Objectif : boucle,
%, sortie anticipée (return/break) ; optimisation possible : ne tester les diviseurs que jusqu'à la racine carrée den(bases du Rust). - Signature suggérée :
fn est_premier(n: u32) -> bool. - Cas de test :
est_premier(2)->true,est_premier(1)->false,est_premier(7)->true,est_premier(15)->false,est_premier(97)->true.
Décomposition en nombres premiers
Écrire un programme qui calcule les facteurs premiers d'un nombre naturel donné.
Exemple d'algorithme
Quels sont les facteurs premiers de 60 ?
- Notre premier diviseur est 2:
- 60 est divisible par 2 et il reste 30,
- 30 est divisible par 2 et il reste 15,
- 15 n'est pas divisible par 2,
- 15 est divisible par 3 et il reste 5,
- 5 n'est pas divisible par 2,
- 5 n'est pas divisible par 3,
- 5 n'est pas divisible par 4,
- 5 est divisible par 5 et il reste 1 et nous avons fini.
Nos diviseurs réussis dans ce calcul représentent la liste des facteurs premiers de 60 : 2, 2, 3 et 5. Si cela vous amuse vous pouvez vérifier qu'ils sont bien premiers en utilisant l'exercice précédent.
- Objectif : boucle
while,%, construction d'unVec(voir "Les collections") - ou simple affichage au fil de l'eau. - Signature suggérée :
fn facteurs_premiers(n: u32) -> Vec<u32>. - Cas de test :
facteurs_premiers(60)->[2, 2, 3, 5],facteurs_premiers(13)->[13],facteurs_premiers(1)->[],facteurs_premiers(360)->[2, 2, 2, 3, 3, 5].
Les bases du Rust 1
Ces exercices portent sur les variables, la mutabilité, les constantes, les
tableaux statiques et les structures de contrôle if / for. Faites-les dans
l'ordre, du plus simple au plus complexe. N'utilisez pas de fonctions, contentez-vous
d'afficher les valeurs.
Signe d'un nombre
Renvoyer -1 si le nombre est négatif, 0 s'il est nul, 1 s'il est positif.
- Objectif :
if/else if/else, valeur de retour. - Signature suggérée : -
- Cas de test :
signe(-8)->-1,signe(0)->0,signe(42)->1.
Compte à rebours
Afficher les entiers de 10 à 1, un par ligne, puis "Décollage !".
- Objectif : boucle
for, plage1..=10,println!. - Signature suggérée : -
- Cas de test : la sortie affiche
10,9, ...,1, puisDécollage !.
Moyenne d'un tableau
Calculer la moyenne des cinq nombres d'un tableau [f64; 5].
- Objectif : tableau statique,
forsur les éléments, accumulateur. - Signature suggérée : -
- Cas de test :
moyenne([2.0, 4.0, 4.0, 4.0, 6.0])->4.0,moyenne([1.0, 2.0, 3.0, 4.0, 5.0])->3.0.
Min et max
Afficher le plus petit et le plus grand élément d'un tableau [i32; 8], sous
la forme (min, max).
- Objectif :
for, comparaisons,mut. - Signature suggérée : -
- Cas de test :
min_max([4, 1, 7, 3, 9, 2, 8, 5])->(1, 9),min_max([0, 0, 0, 0, 0, 0, 0, 0])->(0, 0).
FizzBuzz
Pour les entiers de 1 à 100 : afficher "FizzBuzz" si le nombre est divisible par 3 et par 5, sinon "Fizz" s'il est divisible par 3, sinon "Buzz" s'il est divisible par 5, sinon le nombre lui-même.
- Objectif :
for,%,if/else if(ordre des tests). - Signature suggérée : -.
- Cas de test : ligne 3 ->
Fizz, ligne 5 ->Buzz, ligne 15 ->FizzBuzz, ligne 7 ->7.
Les bases du Rust 2
Ces exercices portent sur les fonctions, le if utilisé comme expression, la
boucle for sur des valeurs et le mot-clé return. Faites-les dans l'ordre.
Valeur absolue
Renvoyer la valeur absolue d'un entier, en utilisant le if comme expression
(pas de return, pas de mut).
- Objectif :
let x = if cond { a } else { b };. - Signature suggérée :
fn abs(n: i64) -> i64. - Cas de test :
abs(-5)->5,abs(5)->5,abs(0)->0.
Factorielle
Calculer n! (produit des entiers de 1 à n). Par convention 0! = 1.
- Objectif :
for, accumulateur. - Signature suggérée :
fn factorielle(n: u64) -> u64. - Cas de test :
factorielle(0)->1,factorielle(1)->1,factorielle(5)->120,factorielle(10)->3628800.
PGCD (algorithme d'Euclide)
Calculer le plus grand commun diviseur de deux entiers par modulos successifs.
- Objectif :
while,%, réaffectation de variables. - Signature suggérée :
fn pgcd(a: u64, b: u64) -> u64. - Cas de test :
pgcd(12, 18)->6,pgcd(17, 5)->1,pgcd(0, 7)->7,pgcd(48, 36)->12.
Puissance sans pow
Calculer base élevé à la puissance exp, sans utiliser i64::pow ni
f64::powi.
- Objectif :
for, accumulateur multiplicatif. - Signature suggérée :
fn puissance(base: i64, exp: u32) -> i64. - Cas de test :
puissance(2, 0)->1,puissance(2, 10)->1024,puissance(-3, 3)->-27.
Nombre de chiffres
Compter les chiffres de l'écriture décimale d'un entier. 0 compte pour un
chiffre.
- Objectif :
while, division entière/= 10. - Signature suggérée :
fn nb_chiffres(n: u64) -> u32. - Cas de test :
nb_chiffres(0)->1,nb_chiffres(7)->1,nb_chiffres(1000)->4,nb_chiffres(123456789)->9.
Les types avancés
Ces exercices portent sur les types énumérés, les fonctions associées et les méthodes, ainsi que le pattern matching. Faites-les dans l'ordre.
Feu tricolore
Définir enum Feu { Rouge, Orange, Vert } et une fonction qui renvoie l'état
suivant du feu : Vert -> Orange -> Rouge -> Vert.
- Objectif :
enumsans données,match. - Signature suggérée :
fn suivant(f: Feu) -> Feu. - Cas de test :
suivant(Feu::Vert)->Feu::Orange,suivant(Feu::Orange)->Feu::Rouge,suivant(Feu::Rouge)->Feu::Vert.
Point cardinal
Définir enum Direction { Nord, Est, Sud, Ouest } et une fonction qui fait
tourner d'un quart de tour vers la droite : Nord -> Est -> Sud -> Ouest -> Nord.
- Objectif :
enum,match. - Signature suggérée :
fn tourner_droite(d: Direction) -> Direction. - Cas de test :
tourner_droite(Direction::Nord)->Direction::Est,tourner_droite(Direction::Ouest)->Direction::Nord.
Forme géométrique
Définir enum Forme { Cercle(f64), Rectangle(f64, f64), Triangle { base: f64, hauteur: f64 } }
et une méthode aire(&self) -> f64.
- Objectif : variantes avec données (tuple et struct),
matchavec liaison de valeurs, méthode prenant&self. - Signature suggérée :
impl Forme { fn aire(&self) -> f64 { /* ... */ } }. - Cas de test :
Forme::Rectangle(3.0, 4.0).aire()->12.0,Forme::Triangle { base: 6.0, hauteur: 2.0 }.aire()->6.0,Forme::Cercle(1.0).aire()-> environ3.14159.
Pierre-feuille-ciseaux
Définir enum Coup { Pierre, Feuille, Ciseaux } et enum Issue { Gagne, Perd, Nul }.
Écrire une fonction qui donne l'issue du premier joueur.
- Objectif :
matchsur un tuple(a, b). - Signature suggérée :
fn jouer(a: Coup, b: Coup) -> Issue. - Cas de test :
jouer(Coup::Pierre, Coup::Ciseaux)->Issue::Gagne,jouer(Coup::Pierre, Coup::Feuille)->Issue::Perd,jouer(Coup::Feuille, Coup::Feuille)->Issue::Nul.
Week-end
Définir enum Jour { Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche }
et une fonction qui dit si c'est le week-end.
- Objectif :
matchavec motifs multiples (Jour::Samedi | Jour::Dimanche) ou_. - Signature suggérée :
fn est_weekend(j: Jour) -> bool. - Cas de test :
est_weekend(Jour::Samedi)->true,est_weekend(Jour::Dimanche)->true,est_weekend(Jour::Lundi)->false.
La généricité et des types composés
Ces exercices portent sur les types génériques, les traits, les tuples et les
attributs #[derive]. Faites-les dans l'ordre.
Maximum générique
Écrire une fonction qui renvoie le plus grand de deux éléments, quel que soit leur type - du moment qu'on peut les comparer.
- Objectif : paramètre générique
<T>, contrainteT: PartialOrd. - Signature suggérée :
fn maximum<T: PartialOrd>(a: T, b: T) -> T. - Cas de test :
maximum(3, 8)->8,maximum(2.5, 1.0)->2.5,maximum("abc", "abd")->"abd".
Paire générique
Définir struct Paire<T> { premier: T, second: T } et une fonction qui renvoie
une nouvelle paire avec les deux champs échangés.
- Objectif :
structgénérique, fonction (ou méthode) générique. - Signature suggérée :
fn echanger<T>(p: Paire<T>) -> Paire<T>. - Cas de test :
echanger(Paire { premier: 1, second: 2 })->Paire { premier: 2, second: 1 }.
Trait Aire
Définir trait Aire { fn aire(&self) -> f64; } et l'implémenter pour
struct Cercle { rayon: f64 } et struct Carre { cote: f64 }.
- Objectif : définition d'un trait,
impl Trait for Type. - Signature suggérée :
trait Aire { fn aire(&self) -> f64; }. - Cas de test :
Carre { cote: 3.0 }.aire()->9.0,Cercle { rayon: 2.0 }.aire()-> environ12.566.
Trait avec méthode par défaut
Définir trait Decrire avec une méthode nom(&self) -> String (obligatoire) et
une méthode decrire(&self) -> String par défaut qui renvoie
format!("Ceci est {}", self.nom()). Implémenter le trait pour un premier type
en ne fournissant que nom, puis pour un second type en redéfinissant aussi
decrire.
- Objectif : méthode par défaut d'un trait, redéfinition.
- Signature suggérée :
trait Decrire { fn nom(&self) -> String; fn decrire(&self) -> String { /* défaut */ } }. - Cas de test : un
Chatqui ne définit quenom(renvoyant"un chat") ->decrire()="Ceci est un chat"; un type qui redéfinitdecrire-> sa propre chaîne.
Point<T> et somme des coordonnées
Définir struct Point<T> { x: T, y: T } et une méthode somme(&self) -> T qui
renvoie x + y, disponible seulement lorsque T supporte l'addition.
- Objectif :
impl<T: ...>avec contrainte (std::ops::Add<Output = T> + Copy). - Signature suggérée :
impl<T: Add<Output = T> + Copy> Point<T> { fn somme(&self) -> T }. - Cas de test :
Point { x: 3, y: 4 }.somme()->7,Point { x: 1.5, y: 2.5 }.somme()->4.0.
La propriété
Ces exercices portent sur la liaison et le déplacement (move), les emprunts
partagés (&) et mutables (&mut) et le déréférencement (*). Faites-les dans
l'ordre.
Somme sans consommer
Calculer la somme des éléments d'un vecteur sans en prendre la propriété : après l'appel, le vecteur doit toujours être utilisable par l'appelant.
- Objectif :
&[i32]en argument,forsur des références. - Signature suggérée :
fn somme(v: &[i32]) -> i32. - Cas de test :
somme(&vec![1, 2, 3, 4])->10,somme(&[])->0.
Doubler en place
Multiplier par 2 chaque élément d'un vecteur, en le modifiant sur place.
- Objectif :
&mut [i32],for x in v.iter_mut(),*x. - Signature suggérée :
fn doubler(v: &mut [i32]). - Cas de test : après
doubler(&mut v)avecv = [1, 2, 3],v == [2, 4, 6].
Échanger deux entiers
Échanger les valeurs pointées par deux références mutables, sans utiliser
std::mem::swap.
- Objectif :
&mut i32, déréférencement*, variable temporaire. - Signature suggérée :
fn echanger(a: &mut i32, b: &mut i32). - Cas de test : après
echanger(&mut x, &mut y)avecx = 1, y = 2, on ax == 2 && y == 1.
Corriger le code
On te donne un extrait qui ne compile pas : une valeur y est utilisée après
avoir été déplacée. Corrige-le sans supprimer de ligne (indice : un emprunt,
ou un .clone()).
- Objectif : lire un message d'erreur du borrow checker, choisir entre
&et.clone(). - Signature suggérée : -
- Cas de test : le code compile et affiche deux fois la même valeur.
Compter les pairs
Compter combien d'éléments d'un vecteur sont pairs, sans consommer le vecteur.
- Objectif :
&[i32],for,%, compteurmut. - Signature suggérée :
fn nb_pairs(v: &[i32]) -> usize. - Cas de test :
nb_pairs(&[1, 2, 3, 4, 6])->3,nb_pairs(&[1, 3, 5])->0.
Modules et visibilité
Ces exercices portent sur les modules (mod), la visibilité (pub), les chemins
(crate::, super::), les sous-modules et les modules en ligne. Faites-les dans
l'ordre.
Découper en modules
On te donne un petit programme où sont mélangées, dans main.rs, les fonctions
aire_rectangle, perimetre_rectangle, km_en_miles et miles_en_km.
Réorganise-le en deux modules-fichiers, geometrie et conversions, en ne
mettant en pub que l'API utile.
- Objectif :
mod nom;+ fichier,pub fn, cheminsgeometrie::aire_rectangle(...). - Signature suggérée : -
- Cas de test :
mainpeut appelergeometrie::aire_rectangle(3.0, 4.0)->12.0etconversions::km_en_miles(10.0)-> environ6.21.
Module en ligne
Définir, directement dans main.rs, mod maths { ... } contenant une fonction
publique carre(x: i64) -> i64 et une fonction privée utilisée en
interne. Appeler carre depuis main.
- Objectif :
mod nom { ... }, visibilité privée par défaut. - Signature suggérée :
mod maths { pub fn carre(x: i64) -> i64 { /* ... */ } }. - Cas de test :
maths::carre(5)->25; appeler la fonction privée depuismaindoit provoquer une erreur de compilation.
Corriger la visibilité
On te donne un module formes avec struct Point { x: f64, y: f64 } et une
fonction, tous privés. Le main ne compile pas. Ajoute les pub nécessaires
(sur le type, sur ses champs, sur la fonction) pour que ça compile.
- Objectif : comprendre que
pub structne rend pas les champs publics. - Signature suggérée : -
- Cas de test :
mainpeut écrireformes::Point { x: 1.0, y: 2.0 }et appeler la fonction.
Sous-module et pub use
Créer banque::comptes::Compte (dans banque/comptes.rs ou en ligne). Ajouter
dans banque un pub use comptes::Compte; pour pouvoir écrire banque::Compte
depuis l'extérieur.
- Objectif : sous-modules, ré-export avec
pub use. - Signature suggérée : -
- Cas de test :
mainaccède au type viabanque::Compteet viabanque::comptes::Compte.
Les petits trucs sympas qui aident au développement
Ces exercices portent sur les commentaires de documentation, les tests unitaires,
assert_eq!, les tests de documentation, rustfmt et clippy. Faites-les dans
l'ordre.
Écrire des tests
Reprends factorielle et pgcd des exercices précédents. Écris un module
#[cfg(test)] mod tests avec, pour chaque fonction, au moins trois assert_eq!
dont un cas limite (0, entrée valant 1, ...).
- Objectif :
#[cfg(test)],#[test],use super::*;,assert_eq!. - Signature suggérée : -
- Cas de test :
cargo testaffiche tous les tests au vert.
Test de documentation
Documente fn celsius_en_fahrenheit(c: f64) -> f64 avec un commentaire ///
contenant un bloc de code exemple, qui sera exécuté comme test par cargo test.
- Objectif : doc-test (
``` ... ```dans un///). - Signature suggérée :
fn celsius_en_fahrenheit(c: f64) -> f64. - Cas de test : l'exemple
assert_eq!(celsius_en_fahrenheit(100.0), 212.0);passe ;celsius_en_fahrenheit(0.0)->32.0.
#[should_panic]
Écris fn racine_entiere(n: i64) -> i64 qui panique si n < 0, et un test
annoté #[should_panic] qui le vérifie.
- Objectif :
panic!,#[should_panic],#[should_panic(expected = "...")]. - Signature suggérée :
fn racine_entiere(n: i64) -> i64. - Cas de test :
racine_entiere(9)->3,racine_entiere(-1)panique.
Nettoyer avec clippy
On te donne un extrait volontairement non idiomatique : if x == true,
if cond { true } else { false }, for i in 0..v.len() { /* ... v[i] ... */ }.
Lance cargo clippy et applique ses suggestions.
- Objectif : lire les lints de clippy et écrire du code idiomatique.
- Signature suggérée : -
- Cas de test :
cargo clippyne remonte plus d'avertissement sur cet extrait.
La gestion des erreurs en Rust
Ces exercices portent sur Option, Result, panic! et l'opérateur ?.
Faites-les dans l'ordre.
Division sûre
Diviser deux entiers, en renvoyant une erreur explicite si le diviseur est nul.
- Objectif :
Result<T, E>, variantesOk/Err. - Signature suggérée :
fn div(a: i32, b: i32) -> Result<i32, String>. - Cas de test :
div(10, 2)->Ok(5),div(7, 0)->Err(...)(message au choix).
Premier élément
Renvoyer le premier élément d'un slice, ou rien s'il est vide - sans indexer
directement (v[0] paniquerait sur un slice vide).
- Objectif :
Option<T>,.first()oumatch. - Signature suggérée :
fn premier(v: &[i32]) -> Option<i32>. - Cas de test :
premier(&[3, 1, 2])->Some(3),premier(&[])->None.
Parser une durée "mm:ss"
Convertir une chaîne de la forme "mm:ss" en nombre de secondes. Renvoyer None
si le format est invalide.
- Objectif :
str::split,str::parse(qui renvoie unResult), passageResult->Optionavec.ok(), opérateur?sur uneOption. - Signature suggérée :
fn parse_mmss(s: &str) -> Option<u32>. - Cas de test :
parse_mmss("03:45")->Some(225),parse_mmss("00:00")->Some(0),parse_mmss("bonjour")->None,parse_mmss("3:60:00")->None.
Erreur typée
Définir enum RacineErreur { Negatif } et fn isqrt(n: i64) -> Result<i64, RacineErreur>
qui renvoie la partie entière de la racine carrée, ou Err(RacineErreur::Negatif)
si n est négatif.
- Objectif : type d'erreur dédié (enum) plutôt qu'une
String. - Signature suggérée :
fn isqrt(n: i64) -> Result<i64, RacineErreur>. - Cas de test :
isqrt(16)->Ok(4),isqrt(17)->Ok(4),isqrt(-1)->Err(RacineErreur::Negatif).
Chaîner avec ?
Écrire fn somme_deux(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>
qui parse deux chaînes en i32 et renvoie leur somme, en utilisant ? sur
chaque parse.
- Objectif :
?surResult, propagation d'erreur, type d'erreur commun. - Signature suggérée :
fn somme_deux(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>. - Cas de test :
somme_deux("2", "3")->Ok(5),somme_deux("2", "x")->Err(...).
Les closures
Ces exercices portent sur les fonctions anonymes, les fonctions d'ordre
supérieur, la capture d'environnement et les traits Fn / FnMut. Faites-les
dans l'ordre.
Appliquer n fois
Écrire une fonction qui applique n fois une fonction f à une valeur de
départ : repeter(f, x, 3) vaut f(f(f(x))).
- Objectif : paramètre
F: Fn(i32) -> i32, boucle. - Signature suggérée :
fn repeter<F: Fn(i32) -> i32>(f: F, x: i32, n: u32) -> i32. - Cas de test :
repeter(|v| v + 1, 0, 5)->5,repeter(|v| v * 2, 1, 10)->1024,repeter(|v| v, 42, 0)->42.
Générateur (compteur)
Écrire une fonction qui renvoie une closure ; chaque appel de cette closure
renvoie l'entier suivant, en commençant à 0.
- Objectif : capture d'une variable
mut, traitFnMut,impl FnMut() -> u32,move. - Signature suggérée :
fn compteur() -> impl FnMut() -> u32. - Cas de test :
let mut c = compteur();puisc()->0,c()->1,c()->2.
Filtre paramétré
Écrire une fonction qui garde les éléments d'un slice satisfaisant un prédicat donné.
- Objectif :
impl Fn(&i32) -> boolen argument, construction d'unVec. - Signature suggérée :
fn garder(v: &[i32], pred: impl Fn(&i32) -> bool) -> Vec<i32>. - Cas de test :
garder(&[1, 2, 3, 4, 5], |x| *x % 2 == 0)->[2, 4],garder(&[1, 2, 3], |_| false)->[].
Composer deux fonctions
Écrire composer(f, g) qui renvoie une closure x -> g(f(x)).
- Objectif : deux paramètres génériques
Fn,impl Fn(i32) -> i32en retour,move. - Signature suggérée :
fn composer<F, G>(f: F, g: G) -> impl Fn(i32) -> i32 where F: Fn(i32) -> i32, G: Fn(i32) -> i32. - Cas de test :
let h = composer(|x| x + 1, |x| x * 3); h(2)->9.
Transformer une Option
À partir d'une Option<i32>, obtenir la valeur multipliée par 10 si elle
existe, sinon -1, en une seule expression.
- Objectif :
Option::map,Option::unwrap_or. - Signature suggérée :
fn dix_fois_ou_moins_un(o: Option<i32>) -> i32. - Cas de test :
dix_fois_ou_moins_un(Some(4))->40,dix_fois_ou_moins_un(None)->-1.
Les itérateurs
Ces exercices portent sur iter / into_iter, map, filter, fold, zip,
les consommateurs (sum, min, max, count...) et l'évaluation paresseuse.
Faites-les dans l'ordre. Essayez de n'utiliser aucune boucle for.
Somme des carrés pairs
Calculer la somme des carrés des entiers pairs de 1 à n inclus, avec une chaîne
d'itérateurs.
- Objectif :
(1..=n),filter,map,sum. - Signature suggérée :
fn somme_carres_pairs(n: u32) -> u32. - Cas de test :
somme_carres_pairs(10)->220(4 + 16 + 36 + 64 + 100),somme_carres_pairs(1)->0.
Produit scalaire
Calculer le produit scalaire de deux slices de même longueur.
- Objectif :
zip,map,sum. - Signature suggérée :
fn produit_scalaire(a: &[i64], b: &[i64]) -> i64. - Cas de test :
produit_scalaire(&[1, 2, 3], &[4, 5, 6])->32,produit_scalaire(&[], &[])->0.
Mot le plus long
Renvoyer le mot le plus long d'une phrase. En cas d'égalité, le premier.
- Objectif :
split_whitespace,foldOption. - Signature suggérée :
fn plus_long(phrase: &str) -> Option<&str>. - Cas de test :
plus_long("le chat dort tranquillement")->Some("tranquillement"),plus_long("")->None,plus_long("a bb cc")->Some("bb").
Réimplémenter count
Écrire une fonction qui compte les éléments d'un itérateur en utilisant
uniquement fold (pas .count()).
- Objectif :
fold, accumulateur. - Signature suggérée :
fn compte<I: Iterator>(it: I) -> usize. - Cas de test :
compte(0..10)->10,compte(vec![1, 2, 3].into_iter())->3,compte(std::iter::empty::<i32>())->0.
Moyennes mobiles (fenêtre de 2)
À partir d'un slice de f64, renvoyer le vecteur des moyennes de chaque paire
d'éléments consécutifs.
- Objectif :
slice::windows(2),map. - Signature suggérée :
fn moyennes_paires(v: &[f64]) -> Vec<f64>. - Cas de test :
moyennes_paires(&[1.0, 3.0, 5.0])->[2.0, 4.0],moyennes_paires(&[10.0])->[].
Les collections
Ces exercices portent sur Vec, String et les slices (&[T], &str).
Faites-les dans l'ordre.
Renverser les mots
Renverser l'ordre des mots d'une phrase : "le chat dort" -> "dort chat le". Les mots sont séparés par des espaces simples.
- Objectif :
split_whitespace,rev,collect::<Vec<_>>(),join(" "). - Signature suggérée :
fn renverser_mots(s: &str) -> String. - Cas de test :
renverser_mots("le chat dort")->"dort chat le",renverser_mots("bonjour")->"bonjour".
Histogramme de lettres
Compter les occurrences de chaque lettre minuscule ASCII (a-z) dans une
chaîne. Les autres caractères sont ignorés.
- Objectif :
chars, indexation d'un[u32; 26]viac as usize - 'a' as usize. - Signature suggérée :
fn compter_lettres(s: &str) -> [u32; 26]. - Cas de test : pour
"abbccc", l'indice 0 (a) vaut1, l'indice 1 (b) vaut2, l'indice 2 (c) vaut3; pour"Rust!",u,s,tvalent1et le reste0(leRmajuscule et le!sont ignorés).
Fusion de deux vecteurs triés
À partir de deux slices déjà triés par ordre croissant, produire un Vec trié
contenant tous leurs éléments.
- Objectif : deux indices qui avancent en parallèle,
Vec::push. - Signature suggérée :
fn fusion(a: &[i32], b: &[i32]) -> Vec<i32>. - Cas de test :
fusion(&[1, 3, 5], &[2, 4, 6])->[1, 2, 3, 4, 5, 6],fusion(&[], &[1, 2])->[1, 2],fusion(&[1, 1], &[1])->[1, 1, 1].
Palindrome
Dire si une chaîne se lit pareil dans les deux sens. Raisonne sur les
caractères (chars), pas sur les octets, et ignore la casse.
- Objectif :
chars,rev, comparaison ; piège de l'indexation d'uneString. - Signature suggérée :
fn est_palindrome(s: &str) -> bool. - Cas de test :
est_palindrome("kayak")->true,est_palindrome("Été")->true,est_palindrome("rust")->false.
Sans doublons
Renvoyer les éléments d'un slice sans doublons, en préservant l'ordre de première apparition.
- Objectif :
Vec,contains,push. - Signature suggérée :
fn sans_doublons(v: &[i32]) -> Vec<i32>. - Cas de test :
sans_doublons(&[1, 2, 1, 3, 2, 4])->[1, 2, 3, 4],sans_doublons(&[])->[].
Lifetimes
Ces exercices portent sur les annotations de durée de vie, les fonctions qui renvoient une référence, l'élision et le pattern NewType. Le chapitre est vu en survol : exercices courts.
Le plus long
Renvoyer la plus longue de deux chaînes empruntées. Ici, il faut annoter la durée de vie.
- Objectif :
fn f<'a>(a: &'a str, b: &'a str) -> &'a str. - Signature suggérée :
fn plus_long<'a>(a: &'a str, b: &'a str) -> &'a str. - Cas de test :
plus_long("court", "beaucoup plus long")->"beaucoup plus long",plus_long("ab", "cd")->"ab".
Premier mot
Renvoyer le premier mot d'une chaîne (jusqu'au premier espace, ou toute la chaîne). Ici aucune annotation n'est nécessaire - explique pourquoi en commentaire.
- Objectif : élision des durées de vie (une seule référence en entrée -> la sortie prend la même).
- Signature suggérée :
fn premier_mot(s: &str) -> &str. - Cas de test :
premier_mot("bonjour le monde")->"bonjour",premier_mot("salut")->"salut",premier_mot("")->"".
Struct qui emprunte
Définir struct Extrait<'a> { texte: &'a str } et une méthode
mot(&self, i: usize) -> Option<&str> qui renvoie le i-ème mot.
- Objectif : paramètre de durée de vie sur une
struct,impl<'a>. - Signature suggérée :
impl<'a> Extrait<'a> { fn mot(&self, i: usize) -> Option<&str> }. - Cas de test :
Extrait { texte: "un deux trois" }.mot(1)->Some("deux"),.mot(9)->None.
Min d'un slice
Renvoyer une référence vers le plus petit élément d'un slice, ou None s'il est
vide.
- Objectif :
fn f<'a>(v: &'a [i32]) -> Option<&'a i32>, ou.iter().min(). - Signature suggérée :
fn min_ref<'a>(v: &'a [i32]) -> Option<&'a i32>. - Cas de test :
min_ref(&[4, 1, 7])->Some(&1),min_ref(&[])->None.
CLI
Ces exercices portent sur les arguments de ligne de commande
(std::env::args), la crate clap et la lecture de fichiers. À faire dans un
projet à part (cargo new). Faites-les dans l'ordre.
Écho des arguments
Écrire un programme qui affiche chacun de ses arguments sur une ligne, sans
clap. Ignore le premier (le nom du binaire).
- Objectif :
std::env::args(),.skip(1),for. - Signature suggérée :
fn main(). - Cas de test :
cargo run -- a b caffichea,b,c(un par ligne).
Additionneur
cargo run -- 1 2 3 4 affiche 10. Chaque argument est un entier ; si l'un
n'est pas un entier, afficher un message d'erreur et quitter avec un code de
sortie non nul.
- Objectif :
args,parse, gestion d'erreur,std::process::exit. - Signature suggérée :
fn main(). - Cas de test :
-- 1 2 3 4->10;-- 1 x 3-> message d'erreur, code de sortie non nul.
clap : salutation
Avec clap (#[derive(Parser)]), un CLI qui accepte --nom <NOM> (obligatoire)
et --repeter <N> (optionnel, défaut 1), et affiche "Bonjour N
fois.
- Objectif :
#[derive(Parser)],#[arg(short, long)], valeur par défaut,Option. - Signature suggérée :
struct Args { nom: String, repeter: u32 }. - Cas de test :
-- --nom Alice --repeter 3affiche 3 lignes "Bonjour Alice !" ;-- --nom Boben affiche 1 ; sans--nom-> message d'usage declap.
Compteur de lignes
cargo run -- fichier.txt affiche le nombre de lignes du fichier. Message
d'erreur lisible si le fichier n'existe pas.
- Objectif : argument,
std::fs::read_to_string,str::lines().count(),?. - Signature suggérée :
fn main() -> Result<(), Box<dyn std::error::Error>>. - Cas de test : sur un fichier de 3 lignes ->
3; sur un chemin inexistant -> message d'erreur, code de sortie non nul.
Unsafe
Ces exercices portent sur les pointeurs bruts (*const T, *mut T), les blocs et
fonctions unsafe, et la liste chaînée. Le chapitre est vu en survol : exercices
courts, avec un bloc unsafe minimal et commenté.
Échanger via pointeurs bruts
Échanger les valeurs pointées par deux *mut i32, dans un bloc unsafe.
- Objectif :
*mut T, déréférencement dansunsafe { ... },std::ptr::swapou lecture/écriture manuelle. - Signature suggérée :
fn echanger(a: *mut i32, b: *mut i32). - Cas de test :
let (mut x, mut y) = (1, 2); echanger(&mut x, &mut y);->x == 2 && y == 1.
Somme d'un buffer
Calculer la somme de n entiers consécutifs à partir d'un pointeur *const i32.
- Objectif :
unsafe fn, arithmétique de pointeurs (p.add(i)), déréférencement. - Signature suggérée :
unsafe fn somme(p: *const i32, n: usize) -> i32. - Cas de test : pour
let v = [1, 2, 3, 4];,unsafe { somme(v.as_ptr(), 4) }->10.
len() sur la liste chaînée
Ajouter à la liste chaînée unsafe du chapitre une méthode len(&self) -> usize
qui suit les pointeurs next jusqu'au pointeur nul.
- Objectif : parcours de pointeurs bruts,
while !p.is_null(),unsafe. - Signature suggérée :
impl LinkedList { fn len(&self) -> usize }. - Cas de test : liste vide ->
0; après troispush->3.
peek()
Ajouter une méthode peek(&self) -> Option<i32> qui renvoie la valeur de tête
sans la retirer.
- Objectif : lire
(*head).dataenunsafesans modifier la liste. - Signature suggérée :
impl LinkedList { fn peek(&self) -> Option<i32> }. - Cas de test : après
push(1); push(2);,peek()->Some(2), et un secondpeek()renvoie encoreSome(2); sur une liste vide ->None.
Exercices avancés
Les exercices que nous proposons ici sont très vagues. L'objectif est de vous donner des thèmes, de choses que vous connaissez du cours de programmation et d'algorithmique de l'année passée et de les implémenter un peu comme vous voulez sans contraintes particulières pour que vous expérimentiez avec le langage (oui vous êtes des grand·e·s maintenant).
Essayez au maximum d'utiliser les concepts de Rust vus en classe.
Vous trouverez une liste ci-dessous, avec un degré de difficulté croissant (plus ou moins). L'idée est d'avancer à votre rythme (ce qui n'est pas équivalent à regarder le plafond en attendant que ça passe) et vous donner des idées d'exercices devenant plus complexes au fur et à mesure de votre périple.
- Le nombre secret: implémenter le jeu du nombre secret qui serait joué par des IA utilisant des stratégies différentes.
- Calculer pi: avec la méthode des fléchettes.
- Une pile simple: implémenter une pile utilisant un tableau statique comme structure de données.
- Un tri: un tri à bulles, tri à deux piles et/ou un quicksort ça fait l'affaire.
- Couverture de la reine: écrire un code qui permet de trouver pour une position de départ aléatoire toutes les solutions de couverture de la reine pour un tableau 8x8.
- Les fractions: écrire une librairie de fractions en utilisant les traits
Add,Sub, etc. - Les vecteurs: écrire une librairie de vecteurs en utilisant les traits
Add,Sub, etc. - Le jeu du pendu: implémenter le jeu du pendu où les mots sont des chaînes de caractères aléatoires et les lettres tirées sont également tirées au hasard.
- Compter différemment: écrire un programme qui permet de compter comme au tennis où chaque joueur gagne un point avec une probabilité donnée.
- Le puissance 4: simulez le jeu du puissance 4.
- Le jeu de la bataille: simulez le jeu de carte de la bataille entre deux machines.
Aller plus haut
- Ascii art: écrire un convertisseur d'image en ASCII art.
- Simulation du système solaire: écrire un simulateur de système solaire.