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.
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.
fn main() {
println!("Hello, world!"); // println! est une MACRO (le ! l'indique)
}
Cargo est le gestionnaire de paquets ET l'outil de build. À connaître absolument :
| Commande | Rôle |
|---|---|
cargo new mon_projet | Crée un nouveau projet (avec Cargo.toml + src/main.rs) |
cargo build | Compile (binaire dans target/debug/) |
cargo build --release | Compile optimisé (plus lent à compiler, rapide à l'exécution) |
cargo run | Compile et exécute |
cargo check | Vérifie sans produire de binaire (rapide) |
cargo test | Lance les tests |
cargo fmt / cargo clippy | Formatage / linter (bonnes pratiques) |
Cargo.toml = manifeste (nom, version, dépendances appelées « crates »). Cargo.lock = versions exactes verrouillées. Une bibliothèque Rust s'appelle une crate.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
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)
| Catégorie | Types |
|---|---|
| Entiers signés | i8 i16 i32 i64 i128 isize (défaut i32) |
| Entiers non signés | u8 u16 u32 u64 u128 usize |
| Flottants | f32 f64 (défaut f64) |
| Booléen | bool (true/false) |
| Caractère | char (Unicode, 4 octets, avec 'a') |
let x: u64 = 5; ou "42".parse::<i32>().mut ne permet pas ?; 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 !
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
fn f() -> i32 { 5; } ?}), la valeur est libérée automatiquement (appel de drop). Pas de GC, pas de free manuel.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
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.let s1 = String::from("hi");
let s2 = s1.clone(); // copie profonde : s1 ET s2 valides (2 allocations)
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
value borrowed/used here after move → tu utilises une variable après l'avoir déplacée. Solutions : emprunter (&, section suivante) ou clone().let s2 = s1; où s1: String, que vaut s1 ?& (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 !
&T (lecture) ;&mut T (écriture) ;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("!");
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.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é)
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
&mut T peut-on avoir en même temps sur une valeur ?&T signifie…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 }
}
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.'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).
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 = 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).enum Direction { Nord, Sud, Est, Ouest }
// Les variantes peuvent porter des données :
enum Message {
Quitter,
Bouger { x: i32, y: i32 },
Ecrire(String),
}
Option<T> = Some(T) ou None (remplace le null). Result<T,E> = Ok(T) ou Err(E) (gestion d'erreur). Voir section 11.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 Some(v) = option { // si option vaut Some(...), lier v
println!("{}", v);
}
match sur un bool ne compile pas s'il ne traite que true ?// --- 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 = 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).Option au lieu de paniquer ?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),
}
? (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() 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.? sur un Result ?null en Rust ?fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b } // marche pour tout T comparable
}
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") }
}
<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.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
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]
collect(), sum(), for, count(). map/filter ne font que décrire le pipeline..iter().map(...).filter(...) ?| Type | Rô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
Rc<RefCell<T>> = plusieurs propriétaires qui peuvent aussi muter (graphe, arbre partagé). En multi-thread : Arc<Mutex<T>>.use std::thread;
let h = thread::spawn(|| {
println!("depuis un thread");
});
h.join().unwrap(); // attend la fin du thread
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
; final : x + 1 (retour) vs x + 1; (renvoie ()). Source d'erreur n°1.String/Vec après let b = a; ou après l'avoir passée à une fonction → erreur. Entiers = Copy, pas de souci._.mut → « cannot assign twice to immutable variable ».& vers une variable locale → interdit.println!, vec!, format! ont un ! (ce sont des macros).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.mut pour modifier ; const pour les constantes.drop.&T lecture (plusieurs OK) · &mut T écriture (un seul, pas en même temps qu'un &T).'a : garantissent qu'une référence ne survit pas à sa donnée (compilation).Some/None) ; Result gère l'erreur (Ok/Err) ; ? propage._ = reste ; if let pour un seul cas.impl (méthodes &self) ; enum peut porter des données.impl Trait for Type ; #[derive(Debug)] pour {:?}.iter().map().filter().collect().new / build / run / check / test ; --release = optimisé.! : println! vec! format!.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 ! 💪🦀