Tutoriels gratuits et ouverts à toutes et à tous · conçus au Lycée Chevrollier — AngersÀ propos

Série « Parcours Rust » · partie 3 / 8

Rust niveau 2 — Possession et emprunt

Le cœur de Rust : qui possède une donnée, qui peut la lire, qui peut la modifier : niveau 2 du parcours Rust, avec exercices corrigés et QCM.

Développement✎ B. Jousseaume📅 17 octobre 2026⏱ 6 h📶 Débutant

1. Pourquoi parler de mémoire ?

Un programme range ses données à deux endroits.

La pile (stack)Le tas (heap)
ContenuDonnées de taille fixe et connue : nombres, booléens, tableauxDonnées de taille variable : textes, listes qui grandissent
RapiditéTrès rapidePlus lent : il faut demander de la place
LibérationAutomatique à la fin de la fonctionQuelqu’un doit la libérer

La vraie question est donc : qui libère la mémoire du tas, et quand ? Trois réponses existent.

ApprocheLangagesInconvénient
Le programmeur libère à la mainC, C++Oubli (fuite), libération en double, utilisation après libération : des failles
Un ramasse-miettes libère ce qui ne sert plusPython, Java, C#, JavaScriptCoût à l’exécution, pauses imprévisibles
Le compilateur vérifie des règles de possessionRustDes règles à apprendre : c’est l’objet de ce niveau

2. String et &str

Rust a deux types de texte courants, et ce niveau est le bon moment pour les distinguer.

fn main() {
    let fixe: &str = "Bonjour";              // texte écrit dans le programme, non modifiable
    let mut dynamique = String::from("Bon");   // texte possédé, stocké dans le tas
    dynamique.push_str("jour");
    dynamique.push('!');
    println!("{fixe} / {dynamique} ({} octets)", dynamique.len());
}
String&str
NatureUn texte possédé, modifiable s’il est mutUne vue empruntée sur un texte existant
StockageDans le tasLà où se trouve le texte observé
CréerString::from("…"), "…".to_string(), format!(…)Un littéral "…", ou &une_string
Paramètre de fonctionRarementPresque toujours

3. Les trois règles de la possession

fn main() {
    {
        let s = String::from("temporaire");   // s possède le texte
        println!("{s}");
    }                                           // fin de portée : le texte est libéré
    // println!("{s}");  ← erreur : s n'existe plus ici
}

Le déplacement (move)

Que se passe-t-il quand on affecte une String à une autre variable ?

fn main() {
    let s1 = String::from("Chevrollier");
    let s2 = s1;
    println!("{s1}");
}
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:15
  |
2 |     let s1 = String::from("Chevrollier");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1}");
  |               ^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

Le texte n’a pas été copié : sa possession a été déplacée de s1 vers s2, et s1 n’est plus utilisable. Sans cette règle, s1 et s2 libéreraient chacune le même texte à la fin du bloc : une double libération.

Copier explicitement : clone

fn main() {
    let s1 = String::from("Chevrollier");
    let s2 = s1.clone();          // copie complète dans le tas : deux propriétaires, deux textes
    println!("{s1} et {s2}");

    let a = 5;
    let b = a;                    // les types simples sont copiés automatiquement
    println!("{a} et {b}");
}

Les types de taille fixe (entiers, flottants, bool, char, et les tuples qui n’en contiennent que) sont copiés : ils implémentent le trait Copy. Tous les autres sont déplacés.

La possession et les fonctions

fn afficher(texte: String) {         // la fonction prend possession
    println!("{texte}");
}                                    // texte est libéré ici

fn main() {
    let nom = String::from("Lina");
    afficher(nom);
    // afficher(nom);  ← erreur E0382 : nom a été déplacé dans le premier appel
}

Passer une String à une fonction la déplace, exactement comme une affectation. Rendre la valeur à chaque fois serait pénible : c’est pourquoi on emprunte.

4. Emprunter : les références

Une référence permet d’utiliser une valeur sans en prendre possession. On dit qu’on emprunte la valeur. Elle s’écrit avec &.

fn longueur(texte: &String) -> usize {    // emprunt en lecture
    texte.len()
}                                            // rien n'est libéré : texte n'était qu'emprunté

fn main() {
    let nom = String::from("Chevrollier");
    let n = longueur(&nom);
    println!("{nom} contient {n} octets");  // nom est toujours utilisable
}

Pour modifier une valeur empruntée, il faut une référence mutable &mut, et la variable d’origine doit elle-même être mut :

fn ajouter_suffixe(texte: &mut String) {
    texte.push_str(" - BTS SIO");
}

fn main() {
    let mut classe = String::from("SIO1");
    ajouter_suffixe(&mut classe);
    println!("{classe}");
}

La règle d’emprunt

fn main() {
    let mut s = String::from("texte");
    let r1 = &s;
    let r2 = &s;             // plusieurs lecteurs : autorisé
    println!("{r1} {r2}");   // dernière utilisation de r1 et r2

    let r3 = &mut s;         // un écrivain : autorisé, les lecteurs ne servent plus
    r3.push_str(" modifié");
    println!("{r3}");
}
fn main() {
    let mut s = String::from("texte");
    let lecteur = &s;
    let ecrivain = &mut s;      // erreur E0502 : on ne peut pas écrire pendant une lecture
    ecrivain.push('!');
    println!("{lecteur}");
}

Pourquoi cette règle ? Imagine que lecteur observe le texte pendant qu’ecrivain le fait grandir : le texte peut être déplacé ailleurs dans le tas pour avoir plus de place, et lecteur pointerait alors vers une zone libérée. La règle d’emprunt rend cette situation impossible à écrire.

Pas de référence pendante

fn creer() -> &String {           // erreur E0106 : emprunt de quoi ?
    let s = String::from("local");
    &s
}                                   // s est libérée ici : la référence désignerait du vide

La solution est de rendre la valeur elle-même : fn creer() -> String { String::from("local") }. La possession est alors transmise à l’appelant.

5. Les tranches (slices)

Une tranche est une référence vers une partie d’une collection. &str est d’ailleurs une tranche de texte.

fn premier_mot(texte: &str) -> &str {
    for (i, c) in texte.char_indices() {
        if c == ' ' {
            return &texte[..i];        // du début jusqu'à l'espace (exclu)
        }
    }
    texte                              // pas d'espace : tout le texte
}

fn main() {
    let phrase = String::from("Rust est exigeant");
    let mot = premier_mot(&phrase);    // &String est accepté là où &str est attendu
    println!("{mot}");

    let notes = [12, 15, 9, 17, 11];
    let milieu: &[i32] = &notes[1..4]; // tranche de tableau : 15, 9, 17
    println!("{:?}", milieu);
}

Le texte renvoyé par premier_mot est emprunté à phrase : le compilateur interdira de modifier ou de libérer phrase tant que mot sert encore. Dans d’autres langages, un indice calculé sur un texte qui change ensuite devient silencieusement faux.

6. Mini-projet : l’analyseur de phrase

7. Bilan

Exercices corrigés

Face à une erreur de possession, le réflexe du débutant est d’ajouter .clone() partout. Ça compile, mais ça contourne la question. Les corrigés montrent systématiquement la solution par emprunt, puis expliquent quand clone() est vraiment justifié.

Exercice 2.1 — Compile ou pas ?

Voir le corrigé
ProgrammeCompile ?Raison
AOuiUn i32 est Copy : x reste utilisable.
BNona a été déplacée dans b (E0382 sur le println!).
COuiclone() crée une copie : deux propriétaires distincts.
DOuiUn tuple de types Copy est lui-même Copy.
ENonLe tuple contient une String, il n’est pas Copy : il est déplacé.

Explication

La question à se poser est toujours : ce type est-il Copy ? Tout ce qui contient une donnée dans le tas (String, Vec…) ne l’est pas, et un type composé n’est Copy que si tous ses éléments le sont.

Exercice 2.2 — Emprunter plutôt que prendre

Voir le corrigé
fn compter_voyelles(texte: &str) -> usize {
    let mut n = 0;
    for c in texte.chars() {
        if "aeiouyàâäéèêëîïôöùûüÿAEIOUYÀÂÄÉÈÊËÎÏÔÖÙÛÜŸ".contains(c) {
            n += 1;
        }
    }
    n
}

fn mettre_en_majuscules(texte: &mut String) {
    *texte = texte.to_uppercase();
}

fn main() {
    let mut texte = String::from("Élève en BTS SIO");
    println!("{} voyelles", compter_voyelles(&texte));
    mettre_en_majuscules(&mut texte);
    println!("{texte}");
    println!("{} voyelles", compter_voyelles("littéral accepté aussi"));
}

Explication

L’étoile de *texte = … signifie « la valeur désignée par la référence » : on remplace le contenu de la String d’origine. Si compter_voyelles prenait une String, l’appel déplacerait texte, qui ne serait plus utilisable ensuite ; et on ne pourrait pas lui passer directement un littéral.

Exercice 2.3 — Réparer

Voir le corrigé
// A : saluer emprunte au lieu de prendre possession
fn saluer(nom: &str) { println!("Bonjour {nom}"); }
fn main() {
    let nom = String::from("Sam");
    saluer(&nom);
    saluer(&nom);
}
// B : deux &mut simultanés (E0499) → on les utilise l'un après l'autre
fn main() {
    let mut liste = String::from("a");
    let r1 = &mut liste;
    r1.push('b');
    let r2 = &mut liste;         // r1 ne sert plus : autorisé
    r2.push('c');
    println!("{liste}");
}
// C : on vide le texte pendant qu'une tranche le lit (E0502)
fn main() {
    let mut texte = String::from("abc");
    let debut = &texte[0..1];
    println!("{debut}");         // on termine la lecture…
    texte.clear();               // …avant de modifier
    println!("[{texte}]");
}
// D : le compilateur ne sait pas de quel paramètre vient le résultat (E0106)
fn plus_long<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}
fn main() {
    println!("{}", plus_long("Rust", "Python"));
}

Explication

A, B et C violent chacun une règle précise : déplacement, double écriture, écriture pendant une lecture. Pour D, la fonction renvoie une référence qui provient soit de a, soit de b : l’annotation 'a (une durée de vie) dit au compilateur que le résultat vit au plus aussi longtemps que les deux paramètres. Le niveau 5 y revient.

Mini-projet

Voir le corrigé
// &str : on lit seulement
fn nombre_mots(texte: &str) -> usize {
    texte.split_whitespace().count()
}

// &str en entrée et en sortie : le résultat est une tranche de l'original
fn mot_le_plus_long(texte: &str) -> &str {
    let mut meilleur = "";
    for mot in texte.split_whitespace() {
        if mot.chars().count() > meilleur.chars().count() {
            meilleur = mot;
        }
    }
    meilleur
}

// String en sortie : on fabrique un texte qui n'existait pas
fn initiales(texte: &str) -> String {
    let mut resultat = String::new();
    for mot in texte.split_whitespace() {
        if let Some(c) = mot.chars().next() {
            resultat.push_str(&c.to_uppercase().to_string());
        }
    }
    resultat
}

// &mut String : on modifie le texte de l'appelant
fn censurer(texte: &mut String, mot: &str) {
    let etoiles = "*".repeat(mot.chars().count());
    *texte = texte.replace(mot, &etoiles);
}

fn main() {
    let mut phrase = String::from("le langage Rust protège la mémoire du programme");
    println!("{} mots", nombre_mots(&phrase));
    println!("plus long : {}", mot_le_plus_long(&phrase));
    println!("initiales : {}", initiales(&phrase));
    censurer(&mut phrase, "mémoire");
    println!("{phrase}");
}

Explication

La signature de chaque fonction raconte ce qu’elle fait de la donnée : lire (&str), modifier (&mut String) ou produire (String). On compte les caractères avec chars().count() et non len(), qui compte les octets : « mémoire » fait 7 caractères mais 8 octets.

Pour aller plus loin

if let Some(c) = … est une écriture raccourcie de match que tu étudieras au niveau 3.

🧠 Testez vos connaissances

5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.

1. Après let s2 = s1; avec s1 de type String, que vaut s1 ?

💬 Le texte n’est pas copié : il change de propriétaire. Sans cela, les deux variables libéreraient le même texte.

2. Quels types sont copiés automatiquement lors d’une affectation ?

💬 Les types dont la donnée est entièrement dans la pile sont Copy. String et Vec ne le sont pas.

3. Quelle combinaison d’emprunts est autorisée au même moment ?

💬 Plusieurs lecteurs ou un seul écrivain. C’est cette règle qui empêche de modifier une donnée pendant qu’on la lit.

4. Quel paramètre privilégier pour une fonction qui lit un texte sans le modifier ?

💬 &str accepte les littéraux et les String empruntées, et laisse la variable de l’appelant utilisable.

5. Que fait &"été"[0..1] ?

💬 « é » occupe deux octets. On découpe aux positions données par char_indices() ou find().

© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.

Pour aller plus loin

Python niveau 1 — Premiers pas

Afficher, calculer, mémoriser dans des variables et dialoguer avec l’utilisateur : niveau 1 du parcours Python, avec exercices corrigés et QCM.

Débutant⏱ 4 à 5 h · 17 octobre 2026