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 ? 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é.
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 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
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
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
