Pourquoi Ownership et borrowing en Rust ?
L'Ownership et borrowing sont des concepts fondamentaux de la sécurité et de l'efficacité dans le langage de programmation Rust. Ils permettent de gérer efficacement les ressources du système tout en protégeant contre les erreurs courantes comme les fuites de mémoire, les double libérations et les accès concurrents non sécurisés.
Un cas d'usage concret est la gestion des fichiers sur un système d'exploitation. En utilisant l'ownership, Rust s'assure que chaque fichier est ouvert une seule fois et fermé correctement. Lorsqu'un fichier est fermé, les ressources associées sont libérées immédiatement, évitant ainsi toute fuite de mémoire.
Prerequis
- Connaissance des bases de Rust (variables, types, structures)
- Compréhension des blocs et des fonctions en Rust
- Une installation de Rust avec
rustc,cargoet un éditeur de code comme VSCode ou IntelliJ IDEA avec le plugin Rust
Concepts fondamentaux
Ownership
L'ownership est une règle fondamentale qui s'assure que chaque valeur a une seule variable propriétaire à tout moment. Lorsque la propriété d'une valeur est transférée, elle devient la propriété de la nouvelle variable.
Schéma mental :
- Un objet est détenu par une personne
- Quand cette personne le passe à une autre, elle le donne et perd la possession
- La nouvelle personne doit prendre soin de l'objet
## ❌ Mauvais : On ne peut pas transférer la propriété d'une variable simple
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 perd la possession, et s2 devient le propriétaire
println!("{}", s1); // Erreur : s1 n'est plus valide ici
}
rust
## ✅ Correct : On peut transférer la propriété avec les fonctions
fn main() {
let s1 = String::from("hello");
let s2 = take_ownership(s1);
println!("{}", s2); // Affiche "hello"
}
fn take_ownership(s: String) -> String {
s
}
Borrowing
Le borrowing permet de partager une valeur sans transférer sa propriété. Il existe trois types de borrowing : immutable borrowing, mutable borrowing et shared borrowing.
Schéma mental :
- Un objet est partagé avec plusieurs personnes
- Chaque personne peut lire l'objet mais ne peut pas le modifier
- Une seule personne peut modifier l'objet à la fois
## ❌ Mauvais : On ne peut pas emprunter une valeur mutable plus d'une fois
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Emprunt immuable
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
rust
## ✅ Correct : On peut emprunter une valeur mutable une seule fois
fn main() {
let mut s1 = String::from("hello");
change(&mut s1); // Emprunt mutable
println!("{}", s1);
}
fn change(s: &mut String) {
s.push_str(", world!");
}
Lifetimes
Les lifetimes sont un mécanisme qui assure que les références ne deviennent pas invalides. Ils déterminent la durée pendant laquelle une référence est valide.
Schéma mental :
- L'age d'une personne
- Une référence n'est valide que tant que le propriétaire est vivant
## ❌ Mauvais : On peut avoir une référence qui devient invalide
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
## ✅ Correct : On utilise les lifetimes pour s'assurer que les références sont valides
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Mise en pratique : projet fil rouge
Étape 1 : Création du projet
Commencez par créer un nouveau projet avec cargo :
cargo new ownership_borrowing_project
cd ownership_borrowing_project
Étape 2 : Création des fichiers
Créez les fichiers suivants dans le répertoire src :
main.rsutils.rs
Étape 3 : Implémentation du code
main.rs
mod utils;
fn main() {
let mut task_list = vec!["Task 1", "Task 2"];
utils::add_task(&mut task_list, "Task 3");
println!("Tasks: {:?}", task_list);
}
utils.rs
## ❌ Mauvais : On ne peut pas emprunter une valeur mutable plus d'une fois
pub fn add_task(tasks: &mut Vec<&str>, new_task: &str) {
tasks.push(new_task); // Erreur : on ne peut pas emprunter `tasks` plusieurs fois
}
rust
## ✅ Correct : On utilise les lifetimes pour s'assurer que les références sont valides
pub fn add_task<'a>(tasks: &'a mut Vec<&'a str>, new_task: &'a str) {
tasks.push(new_task);
}
Étape 4 : Exécution du code
Exécutez le projet avec cargo run :
cargo run
Erreurs frequentes et debugging
Erreur :
error[E0505]: cannot move out of a shared reference- Code incorrect :
fn main() { let s1 = String::from("hello"); let len = calculate_length(s1); println!("The length of '{}' is {}.", s1, len); // Erreur : s1 n'est plus valide ici } fn calculate_length(s: String) -> usize { s.len() } - Code correct :
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); // Affiche "The length of 'hello' is 5." } fn calculate_length(s: &String) -> usize { s.len() }
- Code incorrect :
Erreur :
error[E0382]: borrow of moved value- Code incorrect :
fn main() { let mut s1 = String::from("hello"); let len = calculate_length(&mut s1); println!("The length of '{}' is {}.", s1, len); // Erreur : on ne peut pas emprunter `s1` plusieurs fois } fn calculate_length(s: &mut String) -> usize { s.len() } - Code correct :
fn main() { let mut s1 = String::from("hello"); let len = calculate_length(&mut s1); println!("The length of '{}' is {}.", s1, len); // Affiche "The length of 'hello' is 5." } fn calculate_length(s: &mut String) -> usize { s.len() }
- Code incorrect :
Erreur :
error[E0597]: borrowed value does not live long enough- Code incorrect :
pub fn longest<'a>(x: &'a str, y: &str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); // Erreur : on ne peut pas emprunter `string2` plusieurs fois println!("The longest string is {}", result); } - Code correct :
pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); // Affiche "The longest string is xyz" println!("The longest string is {}", result); }
- Code incorrect :
Pour aller plus loin
1. Lifetimes avancés
- Enseigner les lifetimes multiples et la syntaxe complexe.
- Lien vers le Guide de Rust sur les lifetimes.
2. Smart pointers
- Expliquer comment utiliser les smart pointers comme
Box<T>,Rc<T>etRefCell<T>. - Lien vers le Guide de Rust sur les smart pointers.
3. Borrowing vs Ownership
- Comparer les avantages et inconvénients de l'ownership et du borrowing.
- Exemple pratique avec des structures complexes.
Défi pratique
Implémentez une structure Person qui peut avoir plusieurs amis (également des Person). Chaque ami peut envoyer un message à cette personne. Utilisez des lifetimes et des méthodes pour gérer correctement les références et l'ownership.
struct Person {
name: String,
}
impl Person {
fn new(name: &str) -> Person {
Person {
name: name.to_string(),
}
}
fn add_friend(&mut self, friend: &Person) {
// Implémenter la logique ici
}
fn send_message(&self, message: &str, friend: &Person) {
// Implémenter la logique ici
}
}
fn main() {
let alice = Person::new("Alice");
let bob = Person::new("Bob");
alice.add_friend(&bob);
bob.send_message("Hello!", &alice); // Devrait afficher "Alice received a message: Hello!"
}
Ce défi vous permettra d'appliquer les concepts d'ownership, de borrowing et de lifetimes dans une situation réelle.