Contactez-nous

Emprunts (borrowing) : références immuables (`&T`) et mutables (`&mut T`)

Découvrez le concept d'emprunt en Rust : utilisez les références immuables (&T) pour lire et les références mutables (&mut T) pour modifier des données sans en prendre possession.

L'emprunt : accéder aux données sans en prendre la possession

Le système d'ownership de Rust, avec sa sémantique de déplacement par défaut pour les types non-`Copy`, garantit qu'il n'y a qu'un seul propriétaire pour une ressource donnée. Bien que cela assure la sécurité de la mémoire, transférer la propriété à chaque fois qu'une fonction a besoin d'accéder à des données peut devenir lourd et inefficace. Imaginez devoir passer une `String` à une fonction, puis la faire retourner par cette fonction juste pour pouvoir la réutiliser ensuite ! Pour pallier cela, Rust introduit le concept d'emprunt (borrowing) grâce aux références.

Ce sous-chapitre explore en détail comment fonctionnent les références. Une référence vous permet d'accéder à une valeur sans en prendre possession. C'est comme emprunter un livre à un ami : vous pouvez le lire (ou même y écrire si l'ami vous le permet et que c'est un carnet), mais l'ami reste le propriétaire et s'attend à le récupérer. En Rust, le compilateur s'assure que ces "emprunts" sont sûrs.

Nous allons distinguer les deux types de références : les références immuables (`&T`), qui permettent un accès en lecture seule, et les références mutables (`&mut T`), qui permettent de modifier la valeur empruntée. Les règles strictes qui régissent leur utilisation sont la clé pour prévenir les "data races" (conditions de concurrence sur les données) à la compilation.

Références immuables (`&T`) : emprunter pour lire

Une référence immuable vous permet d'accéder à une valeur sans en prendre possession et sans pouvoir la modifier. On crée une référence immuable en préfixant une variable avec l'opérateur `&`. Le type d'une référence immuable à une valeur de type `T` est `&T`.

L'un des grands avantages des références immuables est que vous pouvez en avoir plusieurs pointant vers la même donnée simultanément. C'est sûr car aucune d'elles ne peut modifier la donnée, donc il n'y a pas de risque de conflit.

Voici comment utiliser les références immuables :

fn calculer_longueur(s: &String) -> usize { // `s` est une référence immuable à une String
    s.len() // On peut appeler des méthodes qui ne modifient pas `s` (comme .len())
            // On ne pourrait pas faire `s.push_str("...");` ici
} // `s` sort de la portée, mais comme elle n'est qu'une référence,
  // la String originale n'est pas affectée ou libérée ici.

fn main() {
    let chaine1 = String::from("Bonjour le monde");

    // On passe une référence à `chaine1` à la fonction `calculer_longueur`.
    // `chaine1` n'est pas déplacée, elle reste le propriétaire.
    let longueur = calculer_longueur(&chaine1);

    println!("La longueur de '{}' est {}.", chaine1, longueur);
    // `chaine1` est toujours valide et utilisable ici.

    // On peut avoir plusieurs références immuables en même temps :
    let r1 = &chaine1;
    let r2 = &chaine1;
    println!("r1 pointe vers : {}", r1);
    println!("r2 pointe vers : {}", r2);
    // Tout va bien ici.
}

Passer une référence à une fonction est souvent plus efficace que de transférer la propriété puis de la retourner, surtout pour des données volumineuses, car cela évite des copies de pointeurs et la gestion des transferts de propriété. Les fonctions qui ont seulement besoin de lire des données devraient donc généralement prendre des références immuables en paramètre.

Références mutables (`&mut T`) : emprunter pour modifier

Si vous avez besoin de modifier une valeur que vous avez empruntée, vous devez utiliser une référence mutable. On crée une référence mutable en préfixant une variable (qui doit elle-même être déclarée mutable avec `mut`) avec `&mut`. Le type d'une référence mutable à une valeur de type `T` est `&mut T`.

Les références mutables sont soumises à une règle très stricte : vous ne pouvez avoir qu'une seule référence mutable à une donnée particulière dans une portée donnée. Cette règle est essentielle pour la prévention des data races par Rust. Si vous pouviez avoir deux références mutables pointant vers les mêmes données, elles pourraient essayer de modifier ces données simultanément, conduisant à des résultats imprévisibles ou corrompus, surtout dans un contexte concurrent.

De plus, si vous avez une référence mutable active, vous ne pouvez pas avoir d'autres références (mutables OU immuables) à cette même donnée en même temps. En d'autres termes, soit vous avez plusieurs lecteurs (références immuables), soit vous avez un seul écrivain (une référence mutable), mais jamais les deux en même temps, ni plusieurs écrivains.

Voici comment utiliser les références mutables :

fn ajouter_suffixe(s: &mut String) { // `s` est une référence mutable à une String
    s.push_str(" (modifié)"); // On peut modifier la String via la référence mutable
}

fn main() {
    let mut chaine_modifiable = String::from("Texte initial"); // La variable doit être `mut`

    println!("Avant modification : '{}'", chaine_modifiable);

    ajouter_suffixe(&mut chaine_modifiable); // On passe une référence mutable

    println!("Après modification : '{}'", chaine_modifiable);

    // Démonstration des règles de références mutables :
    let r_mut1 = &mut chaine_modifiable;
    // let r_mut2 = &mut chaine_modifiable; // ERREUR! On ne peut pas avoir une deuxième réf mutable en même temps.
    // let r_immut = &chaine_modifiable; // ERREUR! On ne peut pas avoir de réf immuable si une réf mutable existe.
    
    r_mut1.push_str(" encore"); // On peut utiliser r_mut1 pour modifier
    println!("Après r_mut1 : '{}'", r_mut1); // ou println!("{}", chaine_modifiable) si r_mut1 est sorti de portée.
                                         // La portée d'une référence peut parfois se terminer avant la fin du bloc
                                         // si elle n'est plus utilisée (Non-Lexical Lifetimes).

    // Une fois que `r_mut1` n'est plus utilisé (sa portée effective est terminée),
    // on peut créer une nouvelle référence mutable (ou immuable).
    let r_mut_apres = &mut chaine_modifiable;
    r_mut_apres.push_str(" et encore");
    println!("Après r_mut_apres : '{}'", r_mut_apres);
}

Règles de l'emprunt en résumé et prévention des références pendantes

Résumons les règles de l'emprunt (ou règles des références) que le compilateur Rust vérifie :

  1. A tout moment, vous pouvez avoir soit une seule référence mutable, soit un nombre quelconque de références immuables.
  2. Les références doivent toujours être valides.

La deuxième règle signifie que les références ne doivent jamais pointer vers des données qui n'existent plus. C'est ce qu'on appelle une référence pendante (dangling reference). Le compilateur Rust s'assure que cela ne se produise jamais en analysant les durées de vie (lifetimes) des références et des données auxquelles elles pointent. Si une référence pouvait potentiellement survivre aux données qu'elle emprunte, le code ne compilerait pas.

Exemple de ce que Rust prévient (code qui ne compilerait pas) :

/*
fn creer_reference_pendante() -> &String { // Tente de retourner une référence à une String
    let s = String::from("pendante");
    &s // Retourne une référence à `s`
} // Ici, `s` sort de la portée et est libérée. La référence retournée pointerait vers des données invalides !
  // Le compilateur Rust interdirait cela avec une erreur : `cannot return reference to local variable s`

fn main() {
    // let reference_a_rien = creer_reference_pendante();
}
*/

Dans l'exemple ci-dessus (commenté car il ne compile pas), la fonction `creer_reference_pendante` essaie de retourner une référence à la `String s`. Cependant, `s` est créée à l'intérieur de cette fonction et sera donc détruite lorsque la fonction se termine. La référence retournée serait alors pendante. Le compilateur Rust détecte cette situation et l'interdit. La solution serait de retourner la `String` elle-même (transférant la propriété) plutôt qu'une référence.

L'emprunt, avec ses références immuables et mutables et les règles strictes qui les régissent, est un mécanisme fondamental de Rust. Il permet un accès flexible et performant aux données tout en maintenant les garanties de sécurité mémoire. Au début, les règles peuvent sembler contraignantes, mais elles deviennent rapidement une aide précieuse, guidant le développeur vers la conception de code plus sûr et plus robuste. Le vérificateur d'emprunt (borrow checker) du compilateur est votre allié dans ce processus.