Rust — Révision express
📘 Cursus complet ⚡ Antisèche ← Portail
Spécial partiel · l'essentiel en une page

Rust, de zéro à l'examen 🦀

Tout ce qu'il faut pour réviser ce soir : concepts clés, exemples de code, pièges d'examen et quiz. Focus sur ce qui tombe le plus : ownership, borrowing, lifetimes.

⚡ Si tu n'as que 10 minutes Lis les 3 sections ★ (Ownership, Borrowing, Lifetimes) + l'antisèche finale. C'est 70 % des points d'un partiel Rust.

Sommaire

  1. Pourquoi Rust ?
  2. Cargo & outils
  3. Variables & types
  4. Fonctions & flux
  5. Ownership ★
  6. Borrowing ★
  7. Lifetimes ★
  8. Structs & enums
  9. Pattern matching
  10. Collections
  11. Erreurs : Option/Result ★
  12. Traits & génériques
  13. Closures & iterators
  14. Smart pointers
  15. Concurrence
  16. Pièges d'examen
  17. Antisèche finale

1. Pourquoi Rust ?

Rust est un langage compilé, typé statiquement, aussi rapide que le C/C++ mais qui garantit la sûreté mémoire sans ramasse-miettes (garbage collector). Son idée géniale : un vérificateur à la compilation (le borrow checker) interdit les bugs mémoire (use-after-free, double free, data races) avant même d'exécuter le programme.

Le fameux « Hello, world! »
fn main() {
    println!("Hello, world!"); // println! est une MACRO (le ! l'indique)
}

2. Cargo & la chaîne d'outils

Cargo est le gestionnaire de paquets ET l'outil de build. À connaître absolument :

CommandeRôle
cargo new mon_projetCrée un nouveau projet (avec Cargo.toml + src/main.rs)
cargo buildCompile (binaire dans target/debug/)
cargo build --releaseCompile optimisé (plus lent à compiler, rapide à l'exécution)
cargo runCompile et exécute
cargo checkVérifie sans produire de binaire (rapide)
cargo testLance les tests
cargo fmt / cargo clippyFormatage / linter (bonnes pratiques)
À retenir Cargo.toml = manifeste (nom, version, dépendances appelées « crates »). Cargo.lock = versions exactes verrouillées. Une bibliothèque Rust s'appelle une crate.
❓ Quelle commande compile en version optimisée pour la production ?
cargo run
cargo build --release
cargo check
cargo new

3. Variables, mutabilité, shadowing & types

★ Le réflexe n°1 de Rust Les variables sont immuables par défaut. Pour pouvoir modifier, il faut mut.
let x = 5;        // immuable : x ne peut PAS changer
// x = 6;        // ❌ ERREUR de compilation
let mut y = 5;    // mutable
y = 6;             // ✅ ok

const MAX: u32 = 100_000; // constante : type obligatoire, toujours immuable

Shadowing (masquage)

On peut redéclarer une variable avec let : on crée une nouvelle variable qui « masque » l'ancienne. Différent de mut : on peut même changer de type.

let espaces = "   ";          // type &str
let espaces = espaces.len();  // maintenant un usize ! (shadowing)

Types primitifs

CatégorieTypes
Entiers signési8 i16 i32 i64 i128 isize (défaut i32)
Entiers non signésu8 u16 u32 u64 u128 usize
Flottantsf32 f64 (défaut f64)
Booléenbool (true/false)
Caractèrechar (Unicode, 4 octets, avec 'a')
Inférence de type Rust devine souvent le type. Mais parfois il faut l'annoter : let x: u64 = 5; ou "42".parse::<i32>().
❓ Que permet le shadowing que mut ne permet pas ?
Modifier la valeur
Rendre la variable globale
Changer le type en réutilisant le même nom
Supprimer la variable

4. Fonctions, expressions vs statements & flux

★ Piège d'examen ultra-classique En Rust, presque tout est une expression (qui renvoie une valeur). La dernière expression d'un bloc, SANS point-virgule, est la valeur de retour. Mettre un ; la transforme en statement qui renvoie () (le « unit », rien).
fn plus_un(x: i32) -> i32 {
    x + 1      // PAS de ; → c'est la valeur de retour
}
// Si on écrivait `x + 1;` → ❌ erreur : la fonction renverrait () au lieu de i32

let y = if x > 0 { "positif" } else { "négatif" }; // if est une expression !

Boucles

loop { break; }                 // boucle infinie ; loop peut renvoyer une valeur via break x
while cond { /* ... */ }
for i in 0..5 { }             // 0,1,2,3,4 (0..=5 inclut 5)
for e in &tab { }              // itère sur les éléments
❓ Que renvoie fn f() -> i32 { 5; } ?
Erreur de compilation (le ; fait renvoyer ())
5
0
()

5. Ownership (la possession) ★★★

★ LE concept central de Rust (tombe à TOUS les partiels) Chaque valeur a un seul propriétaire (owner). Quand le propriétaire sort de sa portée (fin du bloc }), la valeur est libérée automatiquement (appel de drop). Pas de GC, pas de free manuel.

Les 3 règles d'or

Move (déplacement) — le piège n°1

Pour les types stockés sur le tas (heap) comme String, Vec, l'affectation déplace la propriété (move). L'ancienne variable devient invalide.

let s1 = String::from("hello");
let s2 = s1;              // la propriété est DÉPLACÉE vers s2
// println!("{}", s1);    // ❌ ERREUR : s1 n'est plus valide (moved)
println!("{}", s2);      // ✅ ok
Copy vs Move Les types simples à taille fixe sur la pile (entiers, bool, char, f64, tuples de Copy) implémentent Copy : ils sont copiés, pas déplacés. Donc let a = 5; let b = a; → a reste utilisable.

clone() — la copie profonde explicite

let s1 = String::from("hi");
let s2 = s1.clone();     // copie profonde : s1 ET s2 valides (2 allocations)

Ownership & fonctions

Passer une valeur à une fonction la déplace aussi (sauf si Copy) :

fn prend(s: String) { println!("{}", s); } // s est détruit à la fin

let texte = String::from("salut");
prend(texte);
// println!("{}", texte); // ❌ texte a été déplacé dans la fonction
Erreur à reconnaître value borrowed/used here after move → tu utilises une variable après l'avoir déplacée. Solutions : emprunter (&, section suivante) ou clone().
❓ Après let s2 = s1; où s1: String, que vaut s1 ?
Une copie de s2
Invalide (la propriété a été déplacée)
Toujours "hello"
Un pointeur nul
❓ Quel type est copié (Copy) et non déplacé ?
i32
String
Vec<i32>
Un struct avec une String

6. Borrowing & références ★★★

★ L'idée Plutôt que de déplacer une valeur, on peut l'emprunter avec & (référence). On accède à la valeur sans en prendre la propriété. L'emprunt se termine, le propriétaire garde sa valeur.
fn longueur(s: &String) -> usize { s.len() } // &String = emprunt

let texte = String::from("salut");
let n = longueur(&texte);   // on PRÊTE texte
println!("{} ({})", texte, n); // ✅ texte est toujours valide !

★ Les règles du borrow checker (à réciter)

La règle d'or des emprunts À un instant donné, pour une valeur, tu peux avoir SOIT :
  • un nombre quelconque de références immuables &T (lecture) ;
  • OU une seule référence mutable &mut T (écriture) ;
jamais les deux en même temps. C'est ce qui empêche les data races.
let mut s = String::from("hi");
let r1 = &s;      // ok
let r2 = &s;      // ok : plusieurs lectures simultanées
// let rm = &mut s; // ❌ interdit tant que r1/r2 sont utilisés
println!("{} {}", r1, r2);

let rm = &mut s;  // ✅ ok maintenant (r1,r2 ne sont plus utilisés)
rm.push_str("!");
Erreur à reconnaître cannot borrow `s` as mutable because it is also borrowed as immutable → tu mélanges lecture et écriture. Sépare les usages dans le temps.

Pas de référence pendante (dangling)

Rust interdit de renvoyer une référence vers une valeur détruite :

fn danger() -> &String {   // ❌ erreur
    let s = String::from("x");
    &s                        // s meurt à la fin → référence pendante
}  // Solution : renvoyer String (déplacer la propriété)

Slices

Un slice &str ou &[T] est une référence vers une portion d'une collection (pas de propriété).

let s = String::from("hello world");
let mot = &s[0..5];   // "hello" — un &str
❓ Combien de &mut T peut-on avoir en même temps sur une valeur ?
Autant qu'on veut
Zéro
Une seule (et aucune référence immuable en parallèle)
Deux maximum
❓ &T signifie…
un emprunt en lecture seule (immuable)
un emprunt mutable
un déplacement
une copie profonde

7. Lifetimes (durées de vie) ★★

★ À quoi ça sert Les lifetimes indiquent au compilateur combien de temps une référence reste valide, pour garantir qu'une référence ne survit jamais à la donnée qu'elle pointe. La plupart sont inférées ; on les annote seulement quand c'est ambigu.

Syntaxe : 'a (apostrophe + nom). On lit « lifetime a ».

// "la référence renvoyée vit aussi longtemps que la plus courte de x et y"
fn plus_long<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Pourquoi l'annotation ici ? La fonction renvoie une référence, mais le compilateur ne sait pas si elle vient de x ou y. 'a lui dit : « le résultat ne vivra pas plus longtemps que les deux entrées ». Ça évite une référence pendante.
Élision (bonne nouvelle) Dans les cas simples, Rust devine les lifetimes tout seul (règles d'élision). Tu n'écris 'a que quand il y a plusieurs références en entrée ET une en sortie dont l'origine est ambiguë.

Lifetime spécial : 'static = vit pendant toute la durée du programme (ex. les littéraux de chaîne "..." sont &'static str).

❓ À quoi servent les lifetimes ?
À accélérer le programme
À garantir qu'une référence ne survit pas à la donnée pointée
À gérer la mémoire à l'exécution
À créer des threads

8. Structs & enums

Structs

struct User {
    nom: String,
    age: u32,
    actif: bool,
}
impl User {
    fn new(nom: String) -> User {        // fonction associée (pas de self) → User::new(...)
        User { nom, age: 0, actif: true }  // raccourci : nom au lieu de nom: nom
    }
    fn anniv(&mut self) { self.age += 1; }  // méthode (prend self)
}
self, &self, &mut self &self = lecture (le plus courant), &mut self = modifie l'instance, self = consomme l'instance. Une fonction sans self dans impl est une fonction associée (ex. les constructeurs ::new).

Enums

enum Direction { Nord, Sud, Est, Ouest }

// Les variantes peuvent porter des données :
enum Message {
    Quitter,
    Bouger { x: i32, y: i32 },
    Ecrire(String),
}
Les 2 enums stars de la lib standard Option<T> = Some(T) ou None (remplace le null). Result<T,E> = Ok(T) ou Err(E) (gestion d'erreur). Voir section 11.

9. Pattern matching : match, if let

★ match doit être exhaustif Un match doit couvrir tous les cas possibles (sinon erreur de compilation). Le _ capture « tout le reste ».
let dir = Direction::Nord;
match dir {
    Direction::Nord => println!("⬆️"),
    Direction::Sud  => println!("⬇️"),
    _ => println!("↔️"),   // tous les autres cas
}

// Déstructuration + liaison de valeurs :
match msg {
    Message::Ecrire(txt) => println!("{}", txt),
    Message::Bouger { x, y } => println!("{},{}", x, y),
    Message::Quitter => return,
}

if let — match raccourci pour un seul cas

if let Some(v) = option {   // si option vaut Some(...), lier v
    println!("{}", v);
}
❓ Pourquoi ce match sur un bool ne compile pas s'il ne traite que true ?
match doit être exhaustif (il manque le cas false)
bool n'est pas matchable
Il faut un point-virgule
Il faut un _ obligatoire toujours

10. Collections : Vec, String, HashMap

// --- Vec (tableau dynamique) ---
let mut v: Vec<i32> = Vec::new();
v.push(1); v.push(2);
let v2 = vec![1, 2, 3];       // macro pratique
let premier = v2[0];          // accès (panique si hors limites)
let sur = v2.get(10);         // renvoie Option (None si absent) → plus sûr

// --- HashMap ---
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Alice", 10);
if let Some(s) = scores.get("Alice") { println!("{}", s); }
String vs &str String = chaîne possédée, modifiable, sur le tas. &str = slice de chaîne empruntée (ex. littéraux "..."). On passe souvent &str en argument (plus flexible).
❓ Quelle méthode d'accès à un Vec renvoie une Option au lieu de paniquer ?
v[i]
v.get(i)
v.push(i)
v.len()

11. Gestion des erreurs : Option, Result, ? ★

★ Rust n'a pas d'exceptions Les erreurs sont des valeurs renvoyées. Deux enums :
  • Option<T> : une valeur peut être absente → Some(v) ou None.
  • Result<T, E> : une opération peut échouer → Ok(v) ou Err(e).
fn diviser(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { return Err(String::from("division par zéro")); }
    Ok(a / b)
}
match diviser(10.0, 2.0) {
    Ok(r)  => println!("= {}", r),
    Err(e) => println!("erreur : {}", e),
}

★ L'opérateur ? (propagation)

Dans une fonction qui renvoie Result, ? renvoie automatiquement l'Err si échec, sinon déballe l'Ok. Très élégant, très demandé en examen.

fn lire() -> Result<String, std::io::Error> {
    let contenu = std::fs::read_to_string("f.txt")?; // si Err → return Err ; sinon continue
    Ok(contenu)
}
unwrap() & expect() — à utiliser avec prudence .unwrap() extrait la valeur mais panique si None/Err. .expect("msg") pareil avec message. OK pour prototyper/tester, à éviter en code robuste. panic!("...") arrête le programme.
❓ Que fait l'opérateur ? sur un Result ?
Il panique toujours
Il ignore l'erreur
Il déballe Ok, ou retourne l'Err de la fonction
Il convertit en Option
❓ Quel type remplace le null en Rust ?
Option<T> (Some/None)
Result<T,E>
nullptr
void

12. Génériques & traits

Génériques

fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }   // marche pour tout T comparable
}

Traits (= interfaces)

Un trait définit un comportement partagé. Les types l'implémentent.

trait Animal {
    fn crier(&self) -> String;             // méthode à implémenter
    fn presenter(&self) { println!("Je dis {}", self.crier()); } // méthode par défaut
}
struct Chien;
impl Animal for Chien {
    fn crier(&self) -> String { String::from("Wouf") }
}
Trait bounds & dérivation <T: Animal> = « T doit implémenter Animal ». #[derive(Debug, Clone)] au-dessus d'un struct génère automatiquement ces traits (ex. Debug pour l'afficher avec {:?}). Traits classiques : Debug, Clone, Copy, PartialEq, Default.
❓ Un trait en Rust, c'est l'équivalent de…
une classe
une interface (comportement partagé)
une variable globale
un pointeur

13. Closures & iterators

Closures (fonctions anonymes)

let ajoute = |a, b| a + b;      // closure
let r = ajoute(2, 3);          // 5
let facteur = 10;
let mult = |x| x * facteur;     // capture `facteur` de l'environnement

Iterators (très puissant & idiomatique)

let v = vec![1, 2, 3, 4];
let somme: i32 = v.iter().sum();
let pairs: Vec<i32> = v.iter()
    .filter(|x| *x % 2 == 0)   // garde les pairs
    .map(|x| x * 10)            // transforme
    .collect();                 // [20, 40]
Lazy Les iterators sont paresseux : rien ne se passe tant qu'on n'appelle pas un « consommateur » comme collect(), sum(), for, count(). map/filter ne font que décrire le pipeline.
❓ Quand s'exécute réellement une chaîne .iter().map(...).filter(...) ?
Immédiatement à chaque étape
Jamais
Seulement quand on appelle un consommateur (collect, sum, for...)
À la compilation

14. Smart pointers : Box, Rc, RefCell

TypeRôle
Box<T>Alloue sur le tas. Propriété unique. Utile pour les types récursifs (listes, arbres) ou gros objets.
Rc<T>Reference Counted : plusieurs propriétaires en lecture (comptage de références). Mono-thread.
RefCell<T>Mutabilité intérieure : emprunts vérifiés à l'exécution (panique si violation). Mono-thread.
Arc<T> / Mutex<T>Versions thread-safe (Arc = Rc atomique ; Mutex = verrou).
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a);   // 2 propriétaires, compteur = 2
println!("{}", Rc::strong_count(&a)); // 2
Combo classique Rc<RefCell<T>> = plusieurs propriétaires qui peuvent aussi muter (graphe, arbre partagé). En multi-thread : Arc<Mutex<T>>.

15. Concurrence

use std::thread;
let h = thread::spawn(|| {
    println!("depuis un thread");
});
h.join().unwrap();   // attend la fin du thread
★ « Fearless concurrency » Les règles d'ownership/borrowing empêchent les data races à la compilation. Pour partager entre threads : Arc<Mutex<T>>. Pour communiquer : les channels (mpsc::channel(), modèle « ne communiquez pas en partageant la mémoire, partagez la mémoire en communiquant »).
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
thread::spawn(move || { tx.send(42).unwrap(); });
println!("{}", rx.recv().unwrap()); // 42

16. Pièges d'examen fréquents 🎯

Question piège type : « ce code compile-t-il ? »
let s1 = String::from("hi");
let s2 = s1;
println!("{} {}", s1, s2);
Réponse : NON. s1 a été déplacé dans s2 ; l'utiliser ensuite est une erreur borrow of moved value. Correctif : let s2 = s1.clone(); ou utiliser &s1.

17. 🧠 Antisèche finale (à relire juste avant)

Dernier conseil En examen, si on te demande « ce code compile-t-il ? », vérifie dans l'ordre : (1) mut présent ? (2) valeur déplacée puis réutilisée ? (3) conflit &/&mut ? (4) ; de retour ? (5) match exhaustif ? Tu attraperas 90 % des pièges. Tu vas gérer ! 💪🦀
⬇️ Télécharger l'antisèche (.md) 📘 Approfondir avec le cursus complet (16 modules)
Révision express Rust · Bon courage pour ton partiel ! 🦀💙