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.

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.
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.
// 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).
// 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.
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.
// 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.
// 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.
Prêt à réussir tes entretiens Rust ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
// 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.
// 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.
// 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<u32>, // 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<Option<String>>,
}
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.
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.
// 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.
// 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<u64>,
}
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<str> {
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>) -> 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.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
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.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Ownership et Borrowing en Rust : le guide complet pour les entretiens techniques
Comprendre en profondeur l'ownership, le borrowing, les lifetimes et le borrow checker en Rust. Un guide essentiel pour se preparer aux entretiens techniques systemes.

Questions d'entretien Rust : Guide complet 2026
Les 25 questions d'entretien Rust les plus fréquentes. Ownership, borrowing, lifetimes, traits, async et concurrence avec réponses détaillées et exemples de code.

Rust : Les bases pour développeurs expérimentés en 2026
Apprenez Rust rapidement en partant de vos acquis. Ownership, borrowing, lifetimes et patterns essentiels expliqués pour développeurs venant de C++, Java ou Python.