Quiz : Gestion essentielle de la mémoire en Rust
(Borrow) checke‑toi avant de te crasher ! 🦀
Prêt·e à mettre à l’épreuve vos compétences en gestion de mémoire Rust ? 🦀
Ce quiz mettra à l’épreuve votre compréhension du système de propriété, des règles d’emprunt, des durées de vie et des pointeurs intelligents de Rust.
Note : Les questions sont formatées sur une largeur d’environ 50 colonnes afin de garantir une lisibilité sur tous les appareils. (Les suggestions d’amélioration sont les bienvenues !)
Que vous soyez un·e Rustacean chevronné·e ou que vous débutiez tout juste avec la gestion de mémoire, ce quiz vous aidera à consolider vos connaissances. Allons-y ! 🦀
Que se passe-t-il lorsque vous exécutez ce code ? Essayez de prédire la sortie ou l’erreur :
fn main() { let philosopher = String::from("Zeno of Citium"); let greeting = philosopher;
println!("Hello, {}!", philosopher);}Ce code ne compile pas à cause des règles de propriété de Rust. Lorsque nous assignons philosopher à greeting, la propriété du String est déplacée vers greeting. Après ce déplacement, philosopher n’est plus valide.
Voici trois façons de corriger cela :
- Cloner la chaîne (crée une nouvelle copie) :
let greeting = philosopher.clone();- Utiliser une référence (emprunte la valeur) :
let greeting = &philosopher;- Utiliser une tranche de chaîne (emprunte une partie de la chaîne) :
let greeting = &philosopher[..];Chaque solution a des cas d’utilisation et des implications de performance différents. Le clonage est plus coûteux mais vous donne la propriété, tandis que les références sont moins chères mais soumises à des contraintes de durée de vie.
Que se passe-t-il lorsque vous exécutez ce code ? Pensez au transfert de propriété :
fn take_knowledge(knowledge: String) { println!("Knowledge: {}", knowledge);}
fn main() { let wisdom = String::from("know thyself"); take_knowledge(wisdom); // What happens to our wisdom? println!("Do you {}", wisdom);}Le code ne compile pas parce que la propriété de wisdom a été déplacée vers take_knowledge et ne peut donc plus être utilisée ensuite.
Voici trois façons de résoudre ce problème :
- Passer par référence (emprunter la valeur) :
fn borrow_it(text: &String) { println!("Inside: {}", text);}borrow_it(&wisdom); // Now wisdom can be used after- Cloner la valeur (créer une nouvelle copie) :
take_knowledge(wisdom.clone()); // Original wisdom remains valid- Retourner la propriété depuis la fonction :
fn take_and_return(text: String) -> String { println!("Inside: {}", text); text // Return ownership back}let wisdom = take_and_return(wisdom); // Reassign returned ownershipChaque approche a des cas d’utilisation différents :
- Références : les plus efficaces, mais nécessitent une gestion des durées de vie
- Clonage : simple mais potentiellement coûteux
- Retour de propriété : utile pour transformer des valeurs
Bonne pratique : utilisez les références sauf si vous avez besoin d’un transfert de propriété.
Que se passe-t-il avec plusieurs références mutables ?
fn main() { let mut wisdom = String::from("He who laughs at"); let ref1 = &mut wisdom; // First mutable borrow let ref2 = &mut wisdom; // Second mutable borrow ref1.push_str(" himself never runs"); ref2.push_str(" out of things to laugh at.");}Réfléchissez aux règles de Rust concernant les références mutables.
Ce code viole les règles fondamentales d’emprunt de Rust :
- Une SEULE référence mutable à une valeur à la fois
- OU n’importe quel nombre de références immuables
- Les références ne peuvent pas survivre à leur référent
Voici comment corriger le code :
- Utiliser un scoping séquentiel :
let mut wisdom = String::from("He who laughs at");{ let ref1 = &mut wisdom; ref1.push_str(" himself never runs");} // ref1 goes out of scopelet ref2 = &mut wisdom; // Now this is validref2.push_str(" out of things to laugh at.");- Ou modifier la chaîne dans un emprunt unique :
let mut wisdom = String::from("He who laughs at");let ref1 = &mut wisdom;ref1.push_str(" himself never runs out of things to laugh at.");Ces règles empêchent les courses de données à la compilation, rendant Rust sûr pour le multithreading par défaut.
Piège fréquent : essayer d’utiliser plusieurs références mutables pour éviter le clonage ou pour modifier différentes parties de la même valeur simultanément.
Ce code compile‑t‑il ? Si oui, pourquoi ? Sinon, qu’est‑ce qui ne va pas ?
fn first_word(s: &str) -> &str { // No explicit lifetimes? match s.find(' ') { Some(pos) => &s[0..pos], None => s, }}
fn main() { let name = String::from("Seneca the Younger"); let first = first_word(&name); println!("Hello, {}", first);}Ce code compile avec succès grâce aux règles d’élision des durées de vie de Rust. Ces règles permettent au compilateur d’inférer automatiquement les durées de vie dans les motifs courants.
Les trois règles d’élision sont :
- Chaque paramètre reçoit son propre paramètre de durée de vie
- S’il n’y a qu’un seul paramètre d’entrée, cette durée de vie est attribuée à tous les paramètres de sortie
- S’il y a plusieurs paramètres d’entrée, mais que l’un d’eux est &self ou &mut self, la durée de vie de self est attribuée à tous les paramètres de sortie
Cette fonction est équivalente à :
fn first_word<'a>(s: &'a str) -> &'a str { // ... same implementation}Motifs courants où l’élision fonctionne :
// These don't need explicit lifetimesfn get_str(s: &str) -> &str { s }fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimesfn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}Bonne pratique : laissez l’élision faire son travail quand c’est possible, mais comprenez quand des durées de vie explicites sont nécessaires.
Qu’est-ce qui ne va pas avec cette définition de type récursif ?
#[derive(Debug)]enum CatList { Cons(i32, CatList), // Recursive without indirection Nil,}
fn main() { let catlist = CatList::Cons(1, CatList::Cons(2, CatList::Cons(3, CatList::Nil)));}Ce code échoue parce que le compilateur ne peut pas déterminer la taille de CatList à la compilation. La nature récursive du type signifie qu’il pourrait être infiniment grand !
Voici comment le corriger en utilisant Box<T> :
#[derive(Debug)]enum CatList { Cons(i32, Box<CatList>), // Box provides a fixed-size pointer Nil,}
fn main() { let catlist = CatList::Cons(1, Box::new(CatList::Cons(2, Box::new(CatList::Cons(3, Box::new(CatList::Nil))))));}Pourquoi Box<T> fonctionne :
- Box fournit un pointeur de taille fixe (généralement 8 octets sur les systèmes 64 bits)
- Les données réelles sont stockées sur le tas
- Le compilateur sait maintenant exactement quel espace allouer
Cas d’utilisation courants de Box<T> :
- Structures de données récursives (listes chaînées, arbres)
- Grandes données que vous voulez garantir d’être allouées sur le tas
- Objets de trait lorsque vous avez besoin d’un dispatch dynamique
Meilleure pratique : utilisez Box<T> lorsque vous avez besoin de :
- Types récursifs
- Garantir l’allocation sur le tas
- Déplacer de grandes données sans les copier
Qu’affichera ce code ? Comptez attentivement !
use std::rc::Rc;
fn main() { let text = Rc::new(String::from("Meditations")); // Count: 1 let marcus = Rc::clone(&text); // What happens here? let aurelius = Rc::clone(&text); // And here? println!( "Reference count: {}", Rc::strong_count(&text) );}Décomposons le fonctionnement de Rc :
- Création initiale avec
Rc::new(): compteur = 1 - Premier clone pour
marcus: compteur = 2 - Second clone pour
aurelius: compteur = 3
Caractéristiques importantes de Rc :
use std::rc::Rc;
fn demonstrate_rc() { let original = Rc::new(String::from("Shared")); println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{ let copy = Rc::clone(&original); println!("Count inside scope: {}", Rc::strong_count(&original)); // 2 } // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1}Points clés :
Rc::clone()est peu coûteux – il ne fait qu’incrémenter un compteur- Rc n’est destiné qu’aux scénarios mono‑thread
- Lorsque la dernière référence est libérée, les données sont nettoyées
- Utilisez les références
Weakpour éviter les cycles de références
Bonnes pratiques :
- Utilisez Rc quand vous avez besoin d’une propriété partagée
- Envisagez
Arcpour les scénarios thread‑safe - Évitez de créer des cycles de références
Cette définition de struct compilera-t-elle ? Pourquoi ou pourquoi pas ?
struct Philosopher { name: &str, // Reference without lifetime quote: &str, // Another reference without lifetime}
fn main() { let phil = Philosopher { name: "Seneca", quote: "Luck happens when preparation meets opportunity", };}Le code échoue parce que les structs contenant des références doivent spécifier des durées de vie. Voici comment le corriger :
// Single lifetime parameterstruct Philosopher<'a> { name: &'a str, quote: &'a str,}
// Or different lifetimes if neededstruct PhilosopherFlex<'n, 'q> { name: &'n str, quote: &'q str,}Modèles courants :
// Own the data insteadstruct PhilosopherOwned { name: String, quote: String,}
// Mixed ownershipstruct PhilosopherMixed<'a> { name: String, // Owned quote: &'a str, // Borrowed}Bonnes pratiques:
- Utilisez des types possédés (String) lorsque vous devez stocker des données indéfiniment
- Utilisez des références lorsque la durée de vie du struct est clairement plus courte que les données
- Envisagez plusieurs paramètres de durée de vie lorsque les références peuvent avoir des durées différentes
- Documentez les relations de durée de vie dans les structures complexes
Que se passe-t-il avec cette fonction qui renvoie la plus longue des deux tranches de chaîne ?
fn longest(text1: &str, text2: &str) -> &str { if text1.len() > text2.len() { text1 // Returning a reference, but which lifetime? } else { text2 // Could be this reference instead }}
fn main() { println!("{}", longest( "Seneca the Younger", "Marcus Aurelius" ));}La référence renvoyée doit rester dans une durée de vie valide pour les deux entrées. Elle ne peut survivre à aucune des valeurs empruntées. Les annotations décrivent cette relation sans prolonger les durées de vie.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}Que se passe-t-il lorsque ce code s’exécute ?
use std::cell::RefCell;
fn main() { let data = RefCell::new(42); let _borrow1 = data.borrow_mut(); // First mutable borrow let _borrow2 = data.borrow_mut(); // Second mutable borrow println!("Value: {}", _borrow2);}Libérez l’emprunt mutable avant de créer des emprunts partagés. Sinon, cet exemple provoque une panique à l’exécution.
use std::cell::RefCell;fn main() { let data = RefCell::new(42); let mut second = data.borrow_mut(); *second += 1; drop(second); let read1 = data.borrow(); let read2 = data.borrow(); println!("{} {}", *read1, *read2);}Qu’affichera ce code ?
use std::cell::Cell;
fn main() { let life = Cell::new(42); let meaning = &life; // Shared reference println!("{}", life.get()); // What prints here? meaning.set(43); // Mutation through shared ref println!("{}", life.get()); // And here?}Cell et RefCell servent des objectifs différents pour la mutabilité intérieure :
use std::cell::{Cell, RefCell};
// Cell for Copy typesstruct Counter { count: Cell<i32>,}
impl Counter { fn increment(&self) { self.count.set(self.count.get() + 1); }}
// RefCell for non-Copy typesstruct Logger { messages: RefCell<Vec<String>>,}
impl Logger { fn log(&self, msg: &str) { self.messages.borrow_mut().push(msg.to_string()); }}Différences clés :
- Cell :
- Fonctionne au mieux avec les types Copy
- Pas d’API d’emprunt
- Copie ou déplace toujours les valeurs
- RefCell :
- Fonctionne avec n’importe quel type
- Possède une API d’emprunt
- Vérification d’emprunt à l’exécution
Bonnes pratiques :
- Utilisez Cell pour les types Copy simples (nombres, bool, etc.)
- Utilisez RefCell quand vous devez emprunter le contenu
- Gardez les mutations via Cell/RefCell au minimum
- Documentez pourquoi la mutabilité intérieure est nécessaire
Quand devez‑vous utiliser Rc (Comptage de références) en Rust ?
Considérez cet exemple :
use std::rc::Rc;
struct SharedConfig { name: String, value: i32,}
fn main() { let config = Rc::new(SharedConfig { name: "settings".to_string(), value: 42, });
let config2 = Rc::clone(&config); // Both config and config2 share ownership}Rc (Comptage de références) est conçu pour les scénarios monothread où vous avez besoin d’une propriété partagée.
Cas d’utilisation courants :
use std::rc::Rc;use std::cell::RefCell;
// Shared ownership in data structuresstruct Node { next: Option<Rc<Node>>, value: i32,}
// Combining with interior mutabilitystruct SharedState { data: Rc<RefCell<Vec<String>>>,}
// Multiple owners of same datalet original = Rc::new(vec![1, 2, 3]);let clone1 = Rc::clone(&original);let clone2 = Rc::clone(&original);Points clés:
- Utilisez Rc lorsque :
- Plusieurs parties de votre code ont besoin de posséder la donnée
- Vous savez que le partage est monothread
- La durée de vie ne peut pas être déterminée statiquement
- Utilisez Arc à la place lorsque :
- Vous avez besoin d’un partage sûr pour les threads
- Plusieurs threads ont besoin de posséder la donnée
- Limitations de Rc :
- Pas sûr pour les threads
- Légère surcharge à l’exécution
- Ne peut pas rompre automatiquement les cycles de références
Bonnes pratiques :
- Privilégiez la propriété unique quand c’est possible
- Utilisez Rc pour un partage monothread
- Utilisez Arc pour les scénarios multithread
- Combinez avec Weak pour éviter les cycles de références
Quelle est la différence clé entre RefCell et RwLock en Rust?
Considérez ces exemples :
use std::cell::RefCell;use std::sync::RwLock;
// Example 1let data = RefCell::new(vec![1, 2, 3]);let borrowed = data.borrow_mut();
// Example 2let shared = RwLock::new(vec![1, 2, 3]);let locked = shared.write().unwrap();RefCell<T> vérifie les emprunts à l’exécution et n’est pas Sync : ne partagez pas une référence entre threads. Il peut être déplacé vers un autre thread si T: Send. RwLock<T> fournit un accès synchronisé, sous réserve de ses contraintes de types.
Que se passe-t-il lorsque ce code s’exécute ?
use std::sync::{Arc, Mutex};
fn main() { let lock = Arc::new(Mutex::new(42)); let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);}Verrouiller deux fois le même mutex dans un thread sans libérer le premier garde de verrouillage ne revient jamais normalement. L’API autorise un interblocage ou une panique ; libérez le garde avant de verrouiller à nouveau.
use std::sync::Mutex;fn main() { let lock = Mutex::new(42); { let mut guard = lock.lock().unwrap(); *guard += 1; } let guard = lock.lock().unwrap(); println!("{}", *guard);}Que se passe-t-il lorsque vous exécutez ce code avec des références faibles ?
use std::rc::{Rc, Weak};
fn main() { let data = Rc::new(String::from("Wisdom")); let weak = Rc::downgrade(&data); // Create weak reference drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());}Les références faibles n’empêchent pas la désallocation de leurs cibles. Voici un exemple détaillé :
use std::rc::{Rc, Weak};use std::cell::RefCell;
// Parent-child tree structure avoiding reference cyclesstruct Node { next: Option<Rc<Node>>, parent: RefCell<Weak<Node>>, // Weak to prevent cycles value: i32,}
impl Node { fn new(value: i32) -> Rc<Node> { Rc::new(Node { next: None, parent: RefCell::new(Weak::new()), value, }) }
fn set_parent(&self, parent: &Rc<Node>) { *self.parent.borrow_mut() = Rc::downgrade(parent); }
fn get_parent(&self) -> Option<Rc<Node>> { self.parent.borrow().upgrade() }}Cas d’utilisation courants :
- Structures de type cache où les entrées peuvent être purgées
- Structures arborescentes avec références parentales
- Patrons d’observateur où les sujets peuvent être supprimés
- Rompre les cycles de références dans des structures de données complexes
Bonnes pratiques :
- Utiliser les références faibles pour les relations optionnelles
- Vérifier les résultats de upgrade() avant de les utiliser
- Documenter clairement les relations de propriété
- Envisager des alternatives comme des indices pour les cas plus simples
Que se passe-t-il avec le descripteur de fichier dans cet exemple RAII ?
use std::fs::File;
struct FileWrapper { file: File,}
fn main() { let file = File::create("test.txt").unwrap(); let wrapper = FileWrapper { file }; // ... use wrapper ... // No Drop implementation}RAII en Rust garantit que les ressources sont correctement gérées. Dans cet exemple, FileWrapper n’a pas besoin d’une implémentation personnalisée de Drop pour que le descripteur de fichier se ferme : son champ File est libéré automatiquement lorsque l’enveloppe sort de la portée.
Vous n’implémentez Drop que lorsque l’enveloppe elle‑même nécessite un nettoyage supplémentaire au‑delà de la libération de ses champs :
use std::fs::File;use std::io::{self, Write};
struct FileWrapper { file: File, path: String,}
impl FileWrapper { fn new(path: &str) -> io::Result<FileWrapper> { Ok(FileWrapper { file: File::create(path)?, path: path.to_string(), }) }
fn write(&mut self, content: &str) -> io::Result<()> { self.file.write_all(content.as_bytes()) }}
impl Drop for FileWrapper { fn drop(&mut self) { // Ensure file is properly closed // Could also do cleanup like deletion println!("Closing file: {}", self.path); }}Patrons RAII :
- Le constructeur acquiert les ressources
- Les méthodes utilisent les ressources en toute sécurité
- Les champs sont libérés automatiquement lorsque le propriétaire sort de la portée
- Un Drop personnalisé ajoute un nettoyage supplémentaire si besoin
- Utilisez
?pour la propagation d’erreurs
Bonnes pratiques :
- S’appuyer sur les implémentations Drop de la bibliothèque standard lorsqu’elles modélisent déjà la ressource
- Garder la gestion des ressources simple et évidente
- Utiliser les types de la bibliothèque standard quand c’est possible
- Documenter le comportement de nettoyage
- Envisager d’utiliser des motifs de garde pour les opérations à portée limitée
Que se passe-t-il lorsqu’on clone cette structure Philosophy ?
#[derive(Clone)]struct Philosophy { school: String, founder: String,}
fn main() { let stoicism = Philosophy { school: String::from("Stoicism"), founder: String::from("Zeno of Citium") }; let new_school = stoicism.clone(); println!("{} - {}", stoicism.school, new_school.school);}Copy effectue une copie implicite bit à bit. Clone est explicite et peut allouer de la mémoire ; ici les deux champs String reçoivent des copies indépendantes. La pile ou le tas ne détermine pas si un type implémente Copy.
Sur une cible Rust 64 bits typique actuelle, quelle est la taille de cette structure ?
struct Metadata { id: u32, // How many bytes? name: String, // How many bytes? active: bool // How many bytes + padding?}Décomposons la disposition mémoire de la struct et son optimisation :
// Typical current 64-bit Rust layout: 32 bytesstruct Metadata { id: u32, // 4 bytes name: String, // 24 bytes on 64-bit systems active: bool // 1 byte + padding/alignment}
// Reordering fields may reduce padding for repr(C) structs,// but default Rust layout is not a stable ABI guarantee.struct OptimizedMetadata { name: String, // 24 bytes id: u32, // 4 bytes active: bool // 1 byte + 3 padding}
// Further optimization with packing#[repr(packed)]struct PackedMetadata { id: u32, active: bool, name: String,}Considérations de disposition mémoire :
- Exigences d’alignement :
- u32 : alignement de 4 octets
- String : alignement de 8 octets et taille de 24 octets sur les cibles 64 bits courantes
- bool : alignement de 1 octet
- Stratégies d’ordre des champs :
- Regrouper les champs de taille similaire
- Placer les alignements les plus grands en premier
- Envisager l’optimisation des lignes de cache
Bonnes pratiques :
- Pour le FFI ou des hypothèses de mise en page stable, utilisez un
repr(...)approprié - Utilisez des tailles d’entiers appropriées
- Envisagez d’utiliser
Optionpour les champs optionnels - Mesurez les structs critiques en taille avec
std::mem::size_of - Utilisez
#[repr(packed)]avec prudence – cela peut affecter les performances
Avec les optimisations activées, que permettent les abstractions d’itérateurs à coût nul dans ces deux implémentations ?
// Implementation A: Iterator fn sum_iterator(v: &[i32]) -> i32 { v.iter().fold(0, |acc, &x| acc + x) }
// Implementation B: Raw loop fn sum_loop(v: &[i32]) -> i32 { let mut sum = 0; for i in 0..v.len() { sum += v[i]; } sum }Les abstractions à coût nul permettent un code optimisé efficace, sans garantir des temps identiques pour chaque compilateur ou niveau d’optimisation. Les deux fonctions calculent la même somme. Mesurez la charge réelle si les performances sont importantes.
use std::ops::Range;trait ZeroCost { fn process(&self) -> u32;}impl ZeroCost for Range<u32> { fn process(&self) -> u32 { self.clone().fold(0, |acc, x| acc + x) }}Merci d’avoir participé au quiz ! Si vous avez aimé tester vos connaissances en Rust, consultez mes autres défis de programmation! 🧠
Vous voulez pousser vos compétences Rust au niveau supérieur ? Voici quelques ressources recommandées :
- Livre Rust – Chapitre 4 : Propriété
- Rust par l’exemple – Gestion de la mémoire
- Référence Rust – Modèle de mémoire
