# Ownership et Borrowing en Rust : Guide complet > Maîtrisez le système d'ownership et borrowing de Rust. Comprendre les règles de propriété, références, lifetimes et les patterns avancés de gestion mémoire. - Published: 2026-02-20 - Updated: 2026-03-28 - Author: SharpSkill - Tags: rust, ownership, borrowing, memory management, systems programming - Reading time: 18 min --- Le système d'ownership est ce qui différencie Rust de tous les autres langages de programmation. Cette approche unique permet de garantir la sécurité mémoire sans garbage collector, détectant les bugs à la compilation plutôt qu'à l'exécution. Ce guide approfondi explore les mécanismes d'ownership et de borrowing, des fondamentaux jusqu'aux patterns avancés utilisés en production. > **Philosophie Rust** > > Le compilateur Rust agit comme un assistant de programmation exigeant : chaque erreur d'ownership bloquée à la compilation représente un bug potentiel évité en production. ## Les trois règles fondamentales de l'ownership Le système d'ownership repose sur trois règles simples mais strictes. Une fois ces règles intégrées, le modèle mental de Rust devient naturel et prévisible. ```rust // ownership_rules.rs // Démonstration des trois règles fondamentales fn main() { // Règle 1 : Chaque valeur a exactement UN propriétaire let s1 = String::from("hello"); // s1 est le propriétaire unique // Règle 2 : Il ne peut y avoir qu'un seul propriétaire à la fois let s2 = s1; // Ownership transféré (move) de s1 vers s2 // println!("{}", s1); // ERREUR de compilation : s1 n'existe plus println!("s2 = {}", s2); // Seul s2 est valide maintenant // Règle 3 : Quand le propriétaire quitte le scope, la valeur est drop { let s3 = String::from("temporary"); println!("s3 dans le bloc = {}", s3); } // s3 est automatiquement libéré ici (drop appelé) // println!("{}", s3); // ERREUR : s3 n'existe plus } ``` Ces trois règles éliminent trois catégories entières de bugs : les use-after-free, les double-free et les fuites mémoire. Le compilateur vérifie statiquement que ces règles sont respectées. ## Move vs Copy : comprendre la sémantique de transfert Le comportement lors de l'assignation dépend du type de données. Les types qui implémentent le trait Copy sont dupliqués, tandis que les autres sont déplacés (moved). ```rust // move_vs_copy.rs // Distinction entre types Copy et types Move fn main() { // Types Copy : valeurs stockées sur la stack, taille connue let x: i32 = 42; let y = x; // x est COPIÉ, pas déplacé println!("x = {}, y = {}", x, y); // Les deux sont valides // Autres types Copy : f64, bool, char, tuples de types Copy let point = (3.0, 4.0); let point_copy = point; // Copie du tuple println!("Original: {:?}, Copie: {:?}", point, point_copy); // Types Move : valeurs sur la heap, taille dynamique let s1 = String::from("owned"); let s2 = s1; // s1 est DÉPLACÉ vers s2 // println!("{}", s1); // ERREUR : valeur déplacée println!("s2 = {}", s2); // Vec, HashMap, Box sont aussi des types Move let vec1 = vec![1, 2, 3]; let vec2 = vec1; // Move, pas copie // println!("{:?}", vec1); // ERREUR println!("vec2 = {:?}", vec2); } // Clone explicite pour dupliquer les types Move fn explicit_clone() { let original = String::from("données importantes"); let clone = original.clone(); // Duplication explicite (coût mémoire) println!("Original: {}", original); // Toujours valide println!("Clone: {}", clone); // Copie indépendante } ``` La distinction Move/Copy est fondamentale : elle détermine si l'assignation transfère la propriété ou crée une copie indépendante. > **Quand utiliser Clone** > > L'appel à `.clone()` doit être intentionnel. Un code rempli de clones peut indiquer un problème de design. Le borrowing est souvent une meilleure solution. ## Borrowing : références immutables et mutables Le borrowing permet d'accéder à une valeur sans en prendre la propriété. C'est le mécanisme qui rend le code Rust à la fois sûr et performant. ```rust // borrowing_basics.rs // Références immutables et mutables fn main() { let s = String::from("hello"); // Référence immutable : lecture seule, plusieurs autorisées let len = calculate_length(&s); // Emprunt immutable println!("'{}' a {} caractères", s, len); // s toujours valide // Plusieurs références immutables simultanées : OK let r1 = &s; let r2 = &s; let r3 = &s; println!("r1={}, r2={}, r3={}", r1, r2, r3); } fn calculate_length(s: &String) -> usize { // s est une référence, pas le propriétaire s.len() } // s sort du scope mais ne drop rien (pas propriétaire) // Références mutables : modification autorisée fn mutable_borrowing() { let mut s = String::from("hello"); change(&mut s); // Emprunt mutable println!("Après modification: {}", s); } fn change(s: &mut String) { s.push_str(", world!"); // Modification via référence mutable } ``` La règle d'or du borrowing : soit plusieurs références immutables, soit une seule référence mutable, jamais les deux simultanément. ## Les règles du borrow checker Le borrow checker est le composant du compilateur qui vérifie les règles de borrowing. Comprendre ses erreurs permet de résoudre rapidement les problèmes. ```rust // borrow_checker_rules.rs // Règles strictes du borrow checker fn main() { // RÈGLE 1 : Pas de référence mutable avec des références immutables let mut s = String::from("hello"); let r1 = &s; // Référence immutable : OK let r2 = &s; // Autre référence immutable : OK // let r3 = &mut s; // ERREUR : ne peut pas emprunter mutable println!("{} et {}", r1, r2); // APRÈS utilisation de r1 et r2, elles sont "mortes" let r3 = &mut s; // Maintenant OK : r1 et r2 plus utilisées r3.push_str(" world"); println!("{}", r3); // RÈGLE 2 : Une seule référence mutable à la fois let mut data = String::from("exclusive"); let ref1 = &mut data; // let ref2 = &mut data; // ERREUR : déjà emprunté mutable ref1.push_str("!"); println!("{}", ref1); } // RÈGLE 3 : Les références ne peuvent pas vivre plus longtemps que la donnée fn dangling_reference_prevented() { let reference; { let s = String::from("short-lived"); // reference = &s; // ERREUR : s ne vit pas assez longtemps } // s est drop ici, reference serait invalide // Solution : déplacer la valeur hors du scope let owned_outside; { let s = String::from("moved out"); owned_outside = s; // Move, pas référence } println!("{}", owned_outside); // OK : owned_outside est propriétaire } ``` Le borrow checker utilise le concept de Non-Lexical Lifetimes (NLL) : une référence est considérée active uniquement jusqu'à sa dernière utilisation, pas jusqu'à la fin du scope. ## Lifetimes : annoter la durée de vie des références Les lifetimes sont des annotations qui aident le compilateur à vérifier que les références restent valides. La plupart du temps, elles sont inférées automatiquement. ```rust // lifetimes_explained.rs // Annotations de durée de vie explicites // Sans annotation : le compilateur infère les lifetimes fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, } } // Avec annotation explicite : même fonction fn first_word_explicit<'a>(s: &'a str) -> &'a str { // 'a signifie : la référence retournée vit aussi longtemps que l'input match s.find(' ') { Some(i) => &s[..i], None => s, } } // Quand les annotations sont nécessaires : plusieurs références fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // Le compilateur ne peut pas deviner quelle référence est retournée if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string"); let result; { let string2 = String::from("xyz"); result = longest(&string1, &string2); println!("Le plus long: {}", result); // OK ici } // println!("{}", result); // ERREUR si décommenté : string2 drop } ``` Les lifetimes ne changent pas la durée de vie des données, elles décrivent les relations entre les durées de vie de différentes références. ## Lifetimes dans les structures Quand une structure contient des références, les lifetimes doivent être annotées pour garantir que la structure ne survit pas aux données référencées. ```rust // struct_lifetimes.rs // Structures contenant des références // Structure avec référence : lifetime obligatoire struct ImportantExcerpt<'a> { part: &'a str, // Cette référence doit vivre au moins aussi longtemps que la struct } impl<'a> ImportantExcerpt<'a> { // Méthode retournant une référence avec le même lifetime fn level(&self) -> i32 { 3 } // Règle d'élision : &self implique le lifetime de sortie fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention: {}", announcement); self.part // Retourne avec le lifetime 'a de self } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let excerpt = ImportantExcerpt { part: first_sentence, // OK : novel vit plus longtemps que excerpt }; println!("Extrait: {}", excerpt.part); println!("Niveau: {}", excerpt.level()); } // Static lifetime : référence valide pour toute la durée du programme fn static_lifetime_example() { let s: &'static str = "Cette string est dans le binaire"; // Les littéraux string ont toujours un lifetime 'static println!("{}", s); } ``` Les règles d'élision de lifetime permettent souvent d'omettre les annotations dans les cas courants, rendant le code plus lisible. ## Patterns avancés : interior mutability Parfois, la mutabilité doit être vérifiée à l'exécution plutôt qu'à la compilation. Rust fournit des types pour ce pattern : RefCell et Cell. ```rust // interior_mutability.rs // Mutabilité intérieure avec RefCell et Cell use std::cell::{Cell, RefCell}; // Cell : pour types Copy, remplace la valeur entière struct Counter { count: Cell, // Mutable malgré &self } impl Counter { fn new() -> Counter { Counter { count: Cell::new(0) } } fn increment(&self) { // Modification via référence immutable ! self.count.set(self.count.get() + 1); } fn get(&self) -> u32 { self.count.get() } } // RefCell : pour types non-Copy, vérifie à l'exécution struct CachedValue { value: RefCell>, } impl CachedValue { fn new() -> CachedValue { CachedValue { value: RefCell::new(None) } } fn get_or_compute(&self, compute: impl FnOnce() -> String) -> String { // borrow() pour lecture, borrow_mut() pour écriture if self.value.borrow().is_none() { *self.value.borrow_mut() = Some(compute()); } self.value.borrow().as_ref().unwrap().clone() } } fn main() { let counter = Counter::new(); counter.increment(); counter.increment(); println!("Compteur: {}", counter.get()); // 2 let cache = CachedValue::new(); let result = cache.get_or_compute(|| { println!("Calcul coûteux..."); String::from("résultat") }); println!("Valeur: {}", result); // Deuxième appel : pas de recalcul let result2 = cache.get_or_compute(|| String::from("jamais exécuté")); println!("Cache hit: {}", result2); } ``` RefCell et Cell déplacent la vérification du borrow checking à l'exécution. Une violation des règles provoque un panic plutôt qu'une erreur de compilation. > **Attention aux panics** > > `RefCell::borrow_mut()` provoque un panic si la valeur est déjà empruntée. Utiliser `try_borrow_mut()` pour une gestion d'erreur explicite. ## Smart pointers et ownership Les smart pointers comme Box, Rc et Arc offrent différentes stratégies de propriété pour des cas d'usage spécifiques. ```rust // smart_pointers.rs // Box, Rc et Arc pour différents patterns d'ownership use std::rc::Rc; use std::sync::Arc; use std::thread; // Box : propriétaire unique, donnée sur la heap fn box_example() { let boxed = Box::new(vec![1, 2, 3, 4, 5]); println!("Boxed vec: {:?}", boxed); // Utile pour : types récursifs, grands objets, trait objects } // Rc : compteur de références, plusieurs propriétaires (single-thread) fn rc_example() { let data = Rc::new(String::from("shared data")); let clone1 = Rc::clone(&data); // Incrémente le compteur let clone2 = Rc::clone(&data); println!("Compteur: {}", Rc::strong_count(&data)); // 3 println!("Tous partagent: {}, {}, {}", data, clone1, clone2); } // Libéré quand le compteur atteint 0 // Arc : Rc thread-safe (Atomic Reference Counting) fn arc_example() { let data = Arc::new(vec![1, 2, 3]); let handles: Vec<_> = (0..3).map(|i| { let data_clone = Arc::clone(&data); thread::spawn(move || { println!("Thread {}: {:?}", i, data_clone); }) }).collect(); for handle in handles { handle.join().unwrap(); } } fn main() { box_example(); rc_example(); arc_example(); } ``` Le choix du smart pointer dépend du pattern de propriété : unique (Box), partagé single-thread (Rc), ou partagé multi-thread (Arc). ## Patterns d'ownership en pratique Voici des patterns courants pour structurer le code autour du système d'ownership. ```rust // ownership_patterns.rs // Patterns pratiques pour la gestion de l'ownership // Pattern 1 : Builder pattern avec ownership chaîné struct RequestBuilder { url: String, headers: Vec<(String, String)>, timeout: Option, } impl RequestBuilder { fn new(url: &str) -> Self { RequestBuilder { url: url.to_string(), headers: Vec::new(), timeout: None, } } // Consomme self et retourne le nouveau self fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self // Retourne l'ownership } fn timeout(mut self, seconds: u64) -> Self { self.timeout = Some(seconds); self } fn build(self) -> Request { Request { url: self.url, headers: self.headers, timeout: self.timeout.unwrap_or(30), } } } struct Request { url: String, headers: Vec<(String, String)>, timeout: u64, } // Pattern 2 : Cow (Copy-on-Write) pour éviter les allocations use std::borrow::Cow; fn process_text(input: &str) -> Cow { if input.contains("REPLACE") { // Allocation seulement si modification nécessaire Cow::Owned(input.replace("REPLACE", "NOUVEAU")) } else { // Pas d'allocation, retourne une référence Cow::Borrowed(input) } } // Pattern 3 : Take pour extraire d'une Option fn extract_value(data: &mut Option) -> String { data.take().unwrap_or_else(|| String::from("default")) // take() remplace par None et retourne l'ownership de la valeur } fn main() { // Builder pattern let request = RequestBuilder::new("https://api.example.com") .header("Authorization", "Bearer token") .header("Content-Type", "application/json") .timeout(60) .build(); println!("URL: {}, Timeout: {}s", request.url, request.timeout); // Cow pattern let text1 = process_text("hello world"); // Pas d'allocation let text2 = process_text("hello REPLACE"); // Allocation println!("{} | {}", text1, text2); // Take pattern let mut optional = Some(String::from("extracted")); let value = extract_value(&mut optional); println!("Valeur: {}, Option: {:?}", value, optional); // None } ``` Ces patterns exploitent le système d'ownership pour créer des APIs ergonomiques et performantes. ## Conclusion Le système d'ownership et borrowing de Rust représente un changement de paradigme dans la gestion mémoire. Une fois maîtrisé, il devient un allié puissant pour écrire du code à la fois performant et sûr. **Points clés à retenir :** ✅ Trois règles d'ownership : un propriétaire unique, transfert de propriété, drop automatique ✅ Borrowing : références immutables multiples OU une référence mutable exclusive ✅ Lifetimes : annoter les relations entre durées de vie des références ✅ Interior mutability : RefCell et Cell pour la mutabilité vérifiée à l'exécution ✅ Smart pointers : Box (unique), Rc (partagé), Arc (thread-safe) ✅ Patterns pratiques : Builder, Cow, Take pour des APIs idiomatiques Le borrow checker peut sembler strict au début, mais chaque erreur qu'il signale représente un bug potentiel évité. Avec la pratique, penser en termes d'ownership devient naturel et améliore la qualité du code dans tous les langages. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/rust/ownership-borrowing-rust-guide-complet