Maîtriser le système de propriété de Rust : le guide complet de la sécurité de la mémoire sans récupération de place
1. Pourquoi la propriété est importante : le problème des 2 000 milliards de dollars
Les bugs de sécurité de la mémoire coûtent à l’industrie du logiciel environ 2 000 milliards de dollars par an. Microsoft rapporte que 70 % de ses failles de sécurité sont des problèmes de sécurité de la mémoire. L'équipe Chrome de Google a trouvé des chiffres similaires. Ces bogues incluent :
- Utilisation après libération : accès à la mémoire qui a été désallouée
- Double-free : Libérer la mémoire deux fois, provoquant une corruption
- Buffer overflows : écriture au-delà de la mémoire allouée
- Courses de données : accès simultané provoquant un comportement imprévisible
- Fuites de mémoire : Oubli de libérer la mémoire allouée
Le compromis traditionnel
Les langages de programmation ont historiquement choisi l’une des deux approches suivantes :
Approche 1 : Gestion manuelle de la mémoire (C/C++)
// Code C - sujet aux erreurs
char* créer_message() {
char* msg = malloc(100);
strcpy(msg, "Bonjour");
renvoyer un message ; // L'appelant doit se rappeler de libérer !
}
processus vide() {
char* m = créer_message();
printf("%s", m);
// J'ai oublié de libérer (m) - fuite de mémoire !
}
Problèmes : Nécessite une discipline parfaite, des erreurs faciles à commettre, des failles de sécurité.
Approche 2 : Garbage Collection (Java/Go/JavaScript)
// Code Java - sûr mais avec une surcharge d'exécution
Chaîne createMessage() {
renvoyer "Bonjour" ; // GC finira par nettoyer
}
// Sûr, mais les pauses GC affectent les performances
Problèmes : pauses imprévisibles, surcharge de mémoire, moins de contrôle sur les performances.
La solution révolutionnaire de Rust
Rust propose une troisième voie : la sécurité de la mémoire sans garbage collection grâce à la vérification de propriété au moment de la compilation. Vous obtenez :
- ✅ Sécurité de la mémoire garantie au moment de la compilation
- ✅ Pas de surcharge d'exécution (abstractions sans coût)
- ✅ Aucune pause du ramasse-miettes
- ✅ Concurrence intrépide (courses aux données impossibles)
- ✅ Performances prévisibles
Le piège ? Vous devez apprendre le système de propriété. Ce guide le rendra très clair.
2. Les trois règles d'or de la propriété
Chaque programme Rust suit ces trois règles, appliquées au moment de la compilation :
Règle 1 : Chaque valeur a un seul propriétaire
fn main() {
let s = String::from("bonjour"); // s possède la chaîne
// Une seule variable à la fois peut posséder ces données
} // s sort de la portée, la mémoire est libérée automatiquement
Règle 2 : lorsque le propriétaire sort du champ d'application, la valeur est supprimée
fn main() {
{
let s = String::from("bonjour"); // s est valide à partir d'ici
// fait des trucs avec s
} // s sort de la portée et est supprimé, mémoire libérée
// println!("{}", s); // ERREUR : s n'existe plus
}
Règle 3 : vous pouvez avoir soit une référence mutable, soit plusieurs références immuables
fn main() {
let mut s = String::from("bonjour");
// Plusieurs références immuables - OK
soit r1 = &s;
soit r2 = &s;
println!("{} et {}", r1, r2);
// Une référence mutable - OK (après r1, r2 ne sont plus utilisés)
soit r3 = &mut s;
r3.push_str("monde");
println!("{}", r3);
}
Pourquoi ces règles ?
- Règles 1 et 2 : évite les fuites de mémoire et les erreurs de double-libération
- Règle 3 : empêche les courses de données au moment de la compilation
3. Sémantique du déplacement : comprendre le transfert de propriété
Le problème : la copie naïve coûte cher
fn main() {
let s1 = String::from("bonjour");
soit s2 = s1 ; // Que se passe-t-il ici ?
// println!("{}", s1); // ERREUR : valeur déplacée vers s2
}
Ce qui se passe réellement :
Pile : Tas :
s1 -> [ptr, len, cap] -> données "bonjour"
|
| (bouger)
v
s2 -> [ptr, len, cap] -> (mêmes données de tas)
Rust déplace la propriété au lieu de copier les données du tas. Après « let s2 = s1 », seul « s2 » est valide. Cela évite :
- Copies complètes coûteuses par défaut
- Erreurs doublement gratuites (seul s2 libérera la mémoire)
Quand Rust copie-t-il au lieu de déplacer ?
Les types qui implémentent le trait « Copier » sont copiés au lieu d'être déplacés :
fn main() {
// Les entiers, les flottants, les booléens, les caractères implémentent Copier
soit x = 5 ;
soit y = x ; // x est copié dans y
println!("x = {}, y = {}", x, y); // Les deux valides !
// Les tuples de types Copy sont également Copy
soit point = (3, 4);
soit point2 = point ; // copié
println!("{:?} et {:?}", point, point2); // Les deux valides !
}
Règle générale : si un type stocke des données sur le tas ou possède des ressources, il n'implémentera pas « Copie ».
Clonage explicite lorsque vous en avez besoin
fn main() {
let s1 = String::from("bonjour");
soit s2 = s1.clone(); // Copie complète explicite
println!("s1 = {}, s2 = {}", s1, s2); // Les deux valides
}
Utilisez le clonage lorsque :
- Vous avez besoin de copies indépendantes
- Le coût de performance est acceptable
- Rend l'intention claire
Évitez de cloner lorsque :
- Vous pouvez restructurer pour recourir à l'emprunt à la place
- Les performances sont essentielles
- Travailler en boucles chaudes
4. Emprunter : des références qui ne sont pas propriétaires
L'emprunt vous permet de référencer des données sans en devenir propriétaire.
Références immuables (&T)
fn main() {
let s = String::from("bonjour");
let len = calculate_length(&s); // Emprunter
println!("La longueur de '{}' est {}", s, len); // est toujours valable !
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s sort du champ d'application, mais ne possède pas les données, donc rien ne se passe
Points clés :
&scrée une référence à s- Les références sont immuables par défaut
- Plusieurs références immuables autorisées
- Le propriétaire d'origine peut toujours lire les données
Références mutables (&mut T)
fn main() {
let mut s = String::from("bonjour");
changer(&mut s); // L'emprunt est modifiable
println!("{}", s); // Affiche "bonjour le monde"
}
fn changement(s : &mut String) {
s.push_str(", monde");
}
Restriction critique : une seule une référence mutable à la fois !
fn main() {
let mut s = String::from("bonjour");
soit r1 = &mut s;
soit r2 = &mut s; // ERREUR : impossible d'emprunter comme mutable plus d'une fois
println!("{}, {}", r1, r2);
}
Pourquoi ? Empêche les courses de données au moment de la compilation :
// C'est impossible en Rust (compilerait en C++)
laissez mut data = vec![1, 2, 3];
laissez ref1 = &mut data;
laissez ref2 = &mut data;
ref1.push(4); // Pourrait réaffecter
ref2.push(5); // Pourrait provoquer une utilisation après libération !
Le secret du vérificateur d'emprunt : durées de vie non lexicales (NLL)
Modern Rust (édition 2018+) est plus intelligent quant à la fin des références :
fn main() {
let mut s = String::from("bonjour");
soit r1 = &s;
soit r2 = &s;
println!("{} et {}", r1, r2);
// r1 et r2 ne sont plus utilisés après ce point
soit r3 = &mut s; // D'ACCORD! r1 et r2 sont "morts"
r3.push_str("monde");
println!("{}", r3);
}
Avant NLL (édition 2015), cela ne serait pas compilé. Désormais, le compilateur suit où les références sont réellement utilisées, et pas seulement leur portée lexicale.
Modèle d'emprunt courant : lectures multiples, écriture unique
fn main() {
laissez mut data = vec![1, 2, 3, 4, 5];
// Phase de lecture : plusieurs emprunts immuables
soit d'abord = &data[0];
let last = &data[data.len() - 1];
println!("premier : {}, dernier : {}", premier, dernier);
// Phase d'écriture : emprunt mutable exclusif
data.push(6);
println!("Mise à jour : {:?}", données);
}
5. Durées de vie : enseigner au compilateur les références
Le problème à vie résolu
fn le plus long(x : &str, y : &str) -> &str {
si x.len() > y.len() {
x // Quelle durée de vie doit avoir le retour ?
} autre {
y // la durée de vie de x ou la durée de vie de y ?
}
}
Le compilateur ne peut pas déterminer si la référence renvoyée est valide :
fn main() {
let string1 = String::from("chaîne longue");
laissez le résultat ;
{
let string2 = String::from("short");
résultat = le plus long (&string1, &string2);
} // string2 déposé ici
// Le résultat est-il valide ? Cela dépend de l'entrée renvoyée !
}
Annotations à vie : contrats explicites
fn le plus long<'a>(x : &'a str, y : &'a str) -> &'a str {
si x.len() > y.len() {
x
} autre {
oui
}
}
Lecture de ceci : "La référence renvoyée sera valide tant que les x et y sont valides."
fn main() {
let string1 = String::from("chaîne longue");
laissez le résultat ;
{
let string2 = String::from("short");
résultat = le plus long (&string1, &string2);
println!("{}", résultat); // OK : les deux sont toujours valides
} // chaîne2 supprimée
// println!("{}", résultat); // ERREUR : string2 a peut-être été renvoyé
}
Élision à vie : quand vous n'avez pas besoin d'annotations
Le compilateur peut déduire des durées de vie selon des modèles courants :
// Aucune annotation nécessaire - durée de vie d'une seule entrée
fn premier_mot(s : &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// Le compilateur voit cela comme :
fn premier_mot<'a>(s : &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
Règles d'élision :
- Chaque référence d'entrée a sa propre durée de vie
- S'il existe exactement une durée de vie d'entrée, la sortie obtient cette durée de vie
- Si la méthode avec
&self, la sortie obtient la durée de vie deself
Durées de vie dans les structures
Lorsque les structures contiennent des références, vous devez annoter les durées de vie :
struct ImportantExcerpt<'a> {
partie : &'a str,
}
fn main() {
let roman = String::from("Appelle-moi Ismaël. Il y a quelques années...");
let first_sentence = roman.split('.').next().unwrap();
let extrait = ImportantExcerpt {
partie : première_phrase,
} ;
println!("{}", extrait.part);
} // extrait et roman abandonnés, tout va bien
Signification : Un « ImportantExcerpt » ne peut pas survivre aux données auxquelles il fait référence.
fn main() {
laissez extrait;
{
let roman = String::from("Appelle-moi Ismaël.");
extrait = ImportantExtrait {
partie : &roman,
} ;
} // ERREUR : roman abandonné, extrait.part suspendu
// println!("{}", extrait.part);
}
La « durée de vie statique »
« statique » signifie « vit pendant toute la durée du programme » :
// Les littéraux de chaîne ont une « durée de vie statique
let s: &'static str = "Je suis stocké dans le binaire";
// Variables statiques
static GLOBAL : &str = "Également 'statique" ;
Erreur courante : N'utilisez pas « statique » juste pour faire disparaître les erreurs !
// MAUVAIS : Forcer 'static à compiler
fn bad_function() -> &'static str {
let s = String::from("bonjour");
// &s // Impossible de revenir - ne vit pas assez longtemps
// Une fuite de mémoire pour obtenir « statique » est une erreur !
}
// BON : renvoie les données possédées à la place
fn good_function() -> Chaîne {
String::from("bonjour")
}
6. Pièges courants et comment les résoudre
Piège 1 : Impossible d'emprunter en tant que mutable car il est déjà emprunté
// ERREUR
fn main() {
soit mut vec = vec![1, 2, 3];
soit d'abord = &vec[0]; // Emprunt immuable
vec.push(4); // ERREUR : emprunt mutable
println!("{}", d'abord);
}
Pourquoi cela échoue : push pourrait être réaffecté, invalidant first.
Solution 1 : Restructurer pour séparer les emprunts
fn main() {
soit mut vec = vec![1, 2, 3];
soit first_value = vec[0]; // Copie la valeur
vec.push(4); // OK : aucun emprunt en cours
println!("{}", first_value);
}
Solution 2 : clonez les données avant de muter
fn main() {
soit mut vec = vec![1, 2, 3];
laissez d'abord = vec.get(0).cloned(); // Option<i32>, pas d'emprunt
vec.push(4);
si let Some(val) = first {
println!("{}", val);
}
}
Piège 2 : Impossible de renvoyer une référence à une variable locale
// ERREUR
fn create_string() -> &String {
let s = String::from("bonjour");
&s // ERREUR : renvoie la référence aux données appartenant à la fonction
} // s est déposé ici, renverrait un pointeur suspendu !
Solution : Renvoyer les données détenues
fn create_string() -> Chaîne {
String::from("hello") // Propriété transférée à l'appelant
}
Piège 3 : impossible de sortir du contenu emprunté
// ERREUR
fn main() {
laissez vec = vec![String::from("a"), String::from("b")];
laissez d'abord = &vec;
laissez pris = vec[0]; // ERREUR : impossible de quitter le contenu indexé
}
Solution 1 : Cloner la valeur
fn main() {
laissez vec = vec![String::from("a"), String::from("b")];
laissez pris = vec[0].clone();
println!("{}", pris);
}
Solution 2 : Utiliser des méthodes qui transfèrent la propriété
fn main() {
laissez mut vec = vec![String::from("a"), String::from("b")];
laisser pris = vec.swap_remove(0); // Prend possession
println!("{}", pris);
}
Piège 4 : emprunts mutables et immuables simultanés
// ERREUR
fn main() {
laissez mut map = HashMap::new();
map.insert("clé", "valeur");
let value = map.get("clé");
map.insert("clé2", "valeur2"); // ERREUR : impossible de muter pendant l'emprunt
println!("{:?}", valeur);
}
Solution : utilisez l'API d'entrée
fn main() {
laissez mut map = HashMap::new();
map.insert("clé", "valeur");
map.entry("key2").or_insert("value2"); // Aucun emprunt conflictuel
if let Some(value) = map.get("key") {
println!("{}", valeur);
}
}
Piège 5 : inadéquation à vie dans les structures
// ERREUR
struct Conteneur {
data : &str, // ERREUR : annotation de durée de vie manquante
}
Solution : Ajouter un paramètre de durée de vie
struct Conteneur<'a> {
données : &'a str,
}
impl<'a> Conteneur<'a> {
fn new(texte : &'a str) -> Soi {
Conteneur { données : texte }
}
fn get_data(&self) -> &str {
données personnelles
}
}
Piège 6 : Invalidation de l'itérateur
// ERREUR
fn main() {
soit mut vec = vec![1, 2, 3, 4, 5];
pour moi dans &vec {
si *je % 2 == 0 {
vec.push(*i * 2); // ERREUR : impossible de modifier pendant l'itération
}
}
}
Solution : Collectez d'abord les indices
fn main() {
soit mut vec = vec![1, 2, 3, 4, 5];
let to_add : Vec<i32> = vec.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collecter();
vec.extend(to_add);
println!("{:?}", vec);
}
7. Modèles avancés : au-delà de la propriété de base
Modèle 1 : Mutabilité intérieure avec RefCell
Parfois, vous devez muter des données avec seulement une référence immuable :
utilisez std :: cell :: RefCell ;
struct Enregistreur {
logs : RefCell<Vec<String>>, // Peut muter via &self
}
impl Enregistreur {
fn new() -> Soi {
Enregistreur {
journaux : RefCell::new(Vec::new()),
}
}
fn log(&self, message: &str) { // Prend &self, pas &mut self
self.logs.borrow_mut().push(message.to_string());
}
fn print_logs(&self) {
pour vous connecter self.logs.borrow().iter() {
println!("{}", journal);
}
}
}
fn main() {
let logger = Logger::new();
logger.log("Premier message");
logger.log("Deuxième message");
logger.print_logs();
}
Quand utiliser :
- Implémentation de caches ou de loggers
- Structures graphiques ou arborescentes avec mutabilité intérieure
- Objets simulés dans les tests
Attention : Vérification des emprunts à l'exécution : panique en cas de violation des règles !
laissez cell = RefCell::new(5);
laissez emprunté1 = cell.borrow();
laissez emprunté2 = cell.borrow_mut(); // PANIQUE : déjà emprunté !
Modèle 2 : Comptage de références avec Rc
Partagez la propriété des données avec plusieurs propriétaires :
utilisez std :: rc :: Rc ;
Nœud struct {
valeur : i32,
enfants : Vec<Rc<Node>>,
}
fn main() {
laissez leaf = Rc::new(Noeud {
valeur : 3,
enfants : vec ![],
});
laissez branch1 = Rc::new(Noeud {
valeur : 1,
enfants : vec![Rc::clone(&leaf)], // Propriété partagée
});
laissez branch2 = Rc::new(Noeud {
valeur : 2,
children: vec![Rc::clone(&leaf)], // Les deux branches possèdent une feuille
});
println!("Nombre de références de feuilles : {}", Rc::strong_count(&leaf)); // 3
}
Points clés :
- Non thread-safe (utilisez
Arcpour les threads) - Le comptage de références a des frais généraux
- Crée des cycles s'il n'est pas prudent (utilisez « Faible » pour briser les cycles)
Modèle 3 : Combinaison de Rc et RefCell
Propriétaires multiples avec mutation :
utilisez std :: rc :: Rc ;
utilisez std :: cell :: RefCell ;
#[dériver (Débogage)]
struct SharedCounter {
nombre : Rc<RefCell<i32>>,
}
impl SharedCounter {
fn new() -> Soi {
Compteur partagé {
nombre : Rc::new(RefCell::new(0)),
}
}
fn incrément(&soi) {
*self.count.borrow_mut() += 1;
}
fn obtenir(&soi) -> i32 {
*self.count.borrow()
}
}
fn main() {
laissez counter1 = SharedCounter::new();
laissez counter2 = SharedCounter {
compte : Rc::clone(&counter1.count),
} ;
counter1.increment();
counter2.increment();
println!("Count : {}", counter1.get()); // 2
}
Modèle 4 : Modèle de constructeur avec propriété
struct Serveur {
hôte : chaîne,
port: u16,
délai d'attente : u64,
}
struct ServerBuilder {
hôte : Option<String>,
port : Option<u16>,
délai d'attente : Option<u64>,
}
impl ServerBuilder {
fn new() -> Soi {
Générateur de serveurs {
hôte : Aucun,
port: aucun,
délai d'attente : aucun,
}
}
fn host(mut self, host: impl Into<String>) -> Self {
self.host = Certains(host.into());
self // Revenir à la propriété
}
port fn (mut self, port : u16) -> Self {
self.port = Certains (port);
soi
}
fn timeout (mut self, timeout : u64) -> Self {
self.timeout = Certains (timeout);
soi
}
fn build(self) -> Résultat<Serveur, &'static str> {
Ok (serveur {
hôte : self.host.ok_or("L'hôte est requis") ?,
port : self.port.unwrap_or(8080),
délai d'attente : self.timeout.unwrap_or(30),
})
}
}
fn main() {
laissez serveur = ServerBuilder::new()
.host("localhost")
.port(3000)
.timeout(60)
.build()
.unwrap();
println!("Serveur : {}:{}", serveur.hôte, serveur.port);
}
Modèle 5 : RAII (l'acquisition de ressources est une initialisation)
La propriété permet le nettoyage automatique des ressources :
utilisez std::fs::Fichier ;
utilisez std::io::{self, Write} ;
struct FichierJournal {
fichier : Fichier,
}
impl LogFile {
fn new(chemin : &str) -> io::Result<Self> {
Ok (Fichier journal {
fichier : Fichier : créer (chemin )?,
})
}
fn write_log(&mut self, message: &str) -> io::Result<()> {
writeln!(self.file, "{}", message)
}
}
impl Drop pour le fichier journal {
fn drop(&mut soi) {
println!("Fermeture du fichier journal");
// Fichier automatiquement fermé lorsqu'il est déposé
}
}
fn main() -> io::Result<()> {
{
let mut log = LogFile::new("app.log")?;
log.write_log("Application démarrée"?);
log.write_log("Traitement des données"?);
} // Fichier automatiquement fermé ici via Drop
println!("Fichier journal fermé automatiquement");
D'accord(())
}
8. Stratégies de refactorisation du monde réel
Scénario 1 : Transmission de données aux fonctions
Avant (combattre le vérificateur d'emprunt) :
struct Utilisateur {
nom : chaîne,
email : chaîne,
}
fn process_user (utilisateur : utilisateur) {
println!("Traitement {}", user.name);
}
fn main() {
laissez l'utilisateur = Utilisateur {
nom : String::from("Alice"),
email : String::from("[email protected]"),
} ;
process_user(utilisateur);
// println!("{}", user.name); // ERREUR : utilisateur déplacé
}
Après (emprunter au lieu de déménager) :
fn process_user(user: &User) { // Emprunter à la place
println!("Traitement {}", user.name);
}
fn main() {
laissez l'utilisateur = Utilisateur {
nom : String::from("Alice"),
email : String::from("[email protected]"),
} ;
process_user(&utilisateur);
println!("{}", user.name); // OK : l'utilisateur est toujours propriétaire
}
Scénario 2 : Travailler avec des collections
Avant :
fn get_first_name(utilisateurs : Vec<Utilisateur>) -> Option<String> {
users.first().map(|u| u.name.clone()) // Clone inutile
}
fn main() {
laissez les utilisateurs = vec![/* ... */];
let name = get_first_name (utilisateurs);
// Je ne peux plus utiliser les utilisateurs - déplacé
}
Après :
fn get_first_name(utilisateurs : &[Utilisateur]) -> Option<&str> {
utilisateurs.first().map(|u| u.name.as_str())
}
fn main() {
laissez les utilisateurs = vec![/* ... */];
let name = get_first_name(&users);
// les utilisateurs sont toujours utilisables
}
Scénario 3 : Struct avec plusieurs champs de chaîne
Avant (beaucoup de clonage) :
fn build_full_name (premier : chaîne, dernier : chaîne) -> Chaîne {
format!("{} {}", premier, dernier)
}
fn main() {
let first = String::from("John");
let last = String::from("Doe");
let full = build_full_name(first.clone(), last.clone());
println!("Premier : {}, Dernier : {}", premier, dernier);
}
Après (utiliser des tranches de chaîne) :
fn build_full_name(premier : &str, dernier : &str) -> Chaîne {
format!("{} {}", premier, dernier)
}
fn main() {
let first = String::from("John");
let last = String::from("Doe");
let full = build_full_name(&first, &last);
println!("Premier : {}, Dernier : {}", premier, dernier);
}
Scénario 4 : mise en cache des résultats
Problème : Besoin d'un cache mutable avec des méthodes immuables
Solution : mutabilité intérieure
utilisez std :: cell :: RefCell ;
utilisez std :: collections :: HashMap ;
struct Calculateur Cher {
cache : RefCell<HashMap<i32, i32>>,
}
impl Calculateur Cher {
fn new() -> Soi {
Calculatrice chère {
cache : RefCell::new(HashMap::new()),
}
}
fn calculate(&self, input: i32) -> i32 { // &self, pas &mut self
// Vérifie le cache
if let Some(&cached) = self.cache.borrow().get(&input) {
retourner en cache ;
}
// Calcul coûteux
soit résultat = entrée * entrée ;
// Stocker en cache
self.cache.borrow_mut().insert(entrée, résultat);
résultat
}
}
fn main() {
let calc = ExpensiveCalculator::new();
println!("{}", calc.calculate(5)); // Calculé
println!("{}", calc.calculate(5)); // Depuis le cache
}
Scénario 5 : Structures arborescentes
Défi : les relations parents-enfants créent des conflits d'emprunt
Solution : Utiliser des indices ou Rc/Weak
utilisez std::rc::{Rc, Faible} ;
utilisez std :: cell :: RefCell ;
Nœud struct {
valeur : i32,
parent : RefCell<Weak<Node>>,
enfants : RefCell<Vec<Rc<Node>>>,
}
Nœud impl {
fn nouveau (valeur : i32) -> Rc<Self> {
Rc::nouveau(Nœud {
valeur,
parent : RefCell::new(Weak::new()),
enfants : RefCell::new(vec![]),
})
}
fn add_child(parent : &Rc<Node>, enfant : Rc<Node>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.enfants.borrow_mut().push(enfant);
}
}
fn main() {
laissez root = Node::new(1);
laissez child1 = Node::new(2);
laissez child2 = Node::new(3);
Node::add_child(&root, child1);
Node::add_child(&root, child2);
println!("La racine a {} enfants", root.children.borrow().len());
}
9. Implications en termes de performances : abstractions à coût nul
La propriété est sans coût
Le système de propriété n'a aucune surcharge d'exécution :
// Ce code Rust :
processus fn (données : Vec<i32>) -> i32 {
data.iter().sum()
}
// Compile dans le même assembly que :
// processus int (int* données, size_t len) {
// somme int = 0 ;
// pour (size_t i = 0; i < len; i++) {
// somme += données[i];
// }
// renvoie la somme ;
// }
Preuve : Vérifiez l'assemblage avec cargo build --release et des outils comme cargo-asm.
Quand le clonage a un coût
// Cher : copie approfondie
soit vec1 = vec![1, 2, 3, 4, 5];
soit vec2 = vec1.clone(); // Alloue une nouvelle mémoire tas, copie tous les éléments
// Gratuit : Référence
soit vec1 = vec![1, 2, 3, 4, 5];
soit vec2 = &vec1; // Pas d'allocation, juste un pointeur
Benchmark : Cloner ou Emprunter
utilisez std::time::Instant ;
fn process_by_value(données : Vec<i32>) -> i32 {
data.iter().sum()
}
fn process_by_reference(data: &[i32]) -> i32 {
data.iter().sum()
}
fn main() {
laissez les données : Vec<i32> = (0..1_000_000).collect();
// Version clone
let start = Instant::now();
pour _ dans 0..1000 {
laissez result = process_by_value(data.clone()); // Cloner à chaque fois
}
println!("Clone : {:?}", start.elapsed());
// Emprunter la version
let start = Instant::now();
pour _ dans 0..1000 {
laissez result = process_by_reference(&data); // Pas de clone
}
println!("Emprunter : {:?}", start.elapsed());
}
// Résultats typiques :
// Clonage : 850 ms
// Emprunter : 120 ms
Frais généraux du pointeur intelligent
// Rc a une petite surcharge
utilisez std :: rc :: Rc ;
utilisez std::time::Instant ;
fn avec_rc(données : Rc<Vec<i32>>) {
let _ = data.len();
}
fn avec_ref(données : &Vec<i32>) {
let _ = data.len();
}
// Rc est 2 mots (pointeur + nombre de références)
// La référence est 1 mot (juste un pointeur)
// Mais Rc permet la propriété partagée là où les références ne le peuvent pas
Quand les frais généraux sont importants :
- Boucles chaudes avec des millions d'itérations
- Systèmes temps réel
- Systèmes embarqués avec ressources limitées
Lorsque les frais généraux n'ont pas d'importance :
- La plupart des codes d'application
- Quand cela permet une meilleure conception
- Quand le clonage coûterait plus cher
10. Guide de migration : à partir d'autres langues
Venant de C++
État d'esprit C++ :
std::string* createString() {
return new std::string("bonjour"); // L'appelant doit supprimer
}
processus vide() {
std::string* s = createString();
std :: cout << * s;
supprimer les s ; // Nettoyage manuel
}
Équivalent rouille :
fn create_string() -> Chaîne {
String::from("hello") // Propriété transférée
}
processus fn() {
soit s = create_string();
println!("{}", s);
} // Supprimé automatiquement
Différences clés :
- Pas de « nouveau »/« suppression » manuelle
- Pas de pointeurs bruts dans le code sécurisé
- Les références sont vérifiées à vie
- Déplacer la sémantique par défaut
Venant de Go
Allez avec l'état d'esprit :
processus func (données [] int) {
data[0] = 100 // Mute l'original
}
fonction main() {
nums := []int{1, 2, 3}
processus (numéros)
fmt.Println(nums) // [100, 2, 3]
}
Rust nécessite une mutabilité explicite :
processus fn (données : &mut [i32]) { // &mut explicite
données[0] = 100 ;
}
fn main() {
laissez mut nums = vec![1, 2, 3]; // mot-clé mut requis
processus(&mut nums); // &mut explicite
println!("{:?}", nums); // [100, 2, 3]
}
Différences clés :
- La mutabilité doit être explicite
- Pas de courses de données cachées
- Les références sont explicites (
&vs valeur)
Venant de Python
État d'esprit Python :
def modifier_list (éléments):
items.append(4) # Mute l'original
nombres = [1, 2, 3]
modifier_liste (numéros)
print(nombres) # [1, 2, 3, 4]
Équivalent rouille :
fn modifier_list(éléments : &mut Vec<i32>) {
articles.push(4);
}
fn main() {
soit mut nums = vec![1, 2, 3];
modifier_list(&mut nums);
println!("{:?}", nums); // [1, 2, 3, 4]
}
Différences clés :
- Tout en Python est une référence ; Rust distingue les valeurs et les références
- Python GC gère le nettoyage ; Rust utilise la propriété
- Python permet la mutation librement ; Rust nécessite
mut
Provenant de JavaScript
État d'esprit JavaScript :
fonction créerUtilisateur() {
return { nom : "Alice", email : "[email protected]" } ;
}
laissez l'utilisateur = createUser();
laissez user2 = utilisateur ; // Copie superficielle
utilisateur2.nom = "Bob" ;
console.log(utilisateur.nom); // "Bob" - les deux font référence au même objet
Comportement de la rouille :
#[dériver(Cloner)]
struct Utilisateur {
nom : chaîne,
email : chaîne,
}
fn create_user() -> Utilisateur {
Utilisateur {
nom : String::from("Alice"),
email : String::from("[email protected]"),
}
}
fn main() {
laissez l'utilisateur = create_user();
laissez user2 = utilisateur ; // Déplacé, pas copié
// println!("{}", user.name); // ERREUR : valeur déplacée
// Si vous souhaitez un comportement de copie :
laissez l'utilisateur = create_user();
laissez user2 = user.clone(); // Clone explicite
println!("{}", user.name); //D'accord
}
Différences clés :
- Les objets JS sont comptés par référence ; Rust se déplace par défaut
- JS a GC ; Rust est propriétaire
- La mutation JS n'est pas restreinte ; Rust applique les règles d'emprunt
Conclusion : adoptez le vérificateur d'emprunt
Le système de propriété semble restrictif au début, mais il est en réalité libérateur :
✅ Aucune fuite de mémoire : S'il compile, la mémoire est gérée correctement ✅ Pas de course aux données : les bugs simultanés sont détectés au moment de la compilation ✅ Pas d'utilisation après-libération : impossible d'accéder à la mémoire libérée ✅ Performances prévisibles : pas de pause GC, pas d'allocations cachées ✅ Refactoring sans peur : le compilateur détecte les modifications importantes
Obtenir des services professionnels →
La courbe d'apprentissage
Semaine 1-2 : Frustration. Le vérificateur d'emprunt rejette tout. Semaine 3-4 : Compréhension. Vous commencez à penser en tant que propriétaire. Mois 2 : Maîtrise. Vous concevez des API qui fonctionnent avec la propriété. Mois 3+ : Maîtrise. Vous écrivez Rust plus rapidement que votre ancien langage.
Conseils pratiques pour apprendre
- Lisez attentivement les erreurs du compilateur : ce sont d'excellents professeurs
- Commencez avec de petits programmes : maîtrisez les bases avant de créer de grands systèmes
- Utilisez généreusement
clone()au début - optimisez plus tard - Ne combattez pas le vérificateur d'emprunt - repensez la conception si vous avez des difficultés
- Étudiez le code de bibliothèque standard - voyez comment les experts procèdent
Prochaines étapes
- Pratique : Résolvez des problèmes sur Exercism, LeetCode ou Advent of Code dans Rust
- Lire : The Rust Book, Rust by Exemple, Rustonomicon (Rust dangereux)
- Build : de vrais projets vous obligent à rencontrer et à résoudre de vrais problèmes
- Contribuer : les projets open source Rust accueillent les nouveaux arrivants
Ressources
- Le langage de programmation Rust (Le livre)
- Rust par l'exemple
- Rustlings - Petits exercices
- Forum des utilisateurs Rust - Posez des questions
- Cette semaine à Rust - Restez à jour