RefCell<T> et le motif de mutabilité interne

La mutabilité interne est un motif de conception en Rust qui vous permet de muter une donnée même s'il existe des références immuables ; normalement, cette action n'est pas autorisée par les règles d'emprunt. Pour muter des données, le motif utilise du code unsafe dans une structure de données pour contourner les règles courantes de Rust qui gouvernent la mutation et l'emprunt. Nous n'avons pas encore parlé du code unsafe ; nous le ferons au chapitre 19. Nous pouvons utiliser des types qui utilisent le motif de mutabilité interne lorsque nous pouvons être sûr que les règles d'emprunt seront suivies au moment de l'exécution, même si le compilateur ne peut pas en être sûr. Le code unsafe concerné est ensuite incorporé dans une API sûre, et le type externe reste immuable.

Découvrons ce concept en examinant le type RefCell<T> qui applique le motif de mutabilité interne.

Appliquer les règles d'emprunt au moment de l'exécution avec RefCell<T>

Contrairement à Rc<T>, le type RefCell<T> représente une propriété unique de la donnée qu'il contient. Qu'est-ce qui rend donc RefCell<T> différent d'un type comme Box<T> ? Souvenez-vous des règles d'emprunt que vous avez apprises au chapitre 4 :

  • A un instant donné, vous pouvez avoir soit (mais pas les deux) une référence mutable, soit n'importe quel nombre de références immuables
  • Les références doivent toujours être en vigueur.

Avec les références et Box<T>, les règles d'emprunt obligatoires sont appliquées au moment de la compilation. Avec RefCell<T>, ces obligations sont appliquées au moment de l'exécution. Avec les références, si vous ne respectez pas ces règles, vous allez obtenir une erreur de compilation. Avec RefCell<T>, si vous ne les respectez pas, votre programme va paniquer et se fermer.

Les avantages de vérifier les règles d'emprunt au moment de la compilation est que les erreurs vont se produire plus tôt dans le processus de développement et qu'il n'y a pas d'impact sur les performances à l'exécution car toute l'analyse a déjà été faite au préalable. Pour ces raisons, la vérification des règles d'emprunt au moment de compilation est le meilleur choix à faire dans la majorité des cas, ce qui explique pourquoi c'est le choix par défaut de Rust.

L'avantage de vérifier les règles d'emprunt plutôt à l'exécution est que cela permet certains scénarios qui restent sûrs pour la mémoire, bien qu'interdits à cause des vérifications à la compilation. L'analyse statique, comme le compilateur Rust, est de nature prudente. Certaines propriétés du code sont impossibles à détecter en analysant le code : l'exemple le plus connu est le problème de l'arrêt, qui dépasse le cadre de ce livre mais qui reste un sujet intéressant à étudier.

Comme certaines analyses sont impossibles, si le compilateur Rust ne peut pas s'assurer que le code respecte les règles d'emprunt, il risque de rejeter un programme valide ; dans ce sens, il est prudent. Si Rust accepte un programme incorrect, les utilisateurs ne pourront pas avoir confiance dans les garanties qu'apporte Rust. Cependant, si Rust rejette un programme valide, le développeur sera importuné, mais rien de catastrophique ne va se passer. Le type RefCell<T> est utile lorsque vous êtes sûr que votre code suit bien les règles d'emprunt mais que le compilateur est incapable de comprendre et de garantir cela.

De la même manière que Rc<T>, RefCell<T> sert uniquement pour des scénarios à une seule tâche et va vous donner une erreur à la compilation si vous essayez de l'utiliser dans un contexte multitâches. Nous verrons comment bénéficier des fonctionnalités de RefCell<T> dans un programme multi-processus au chapitre 16.

Voici un résumé des raisons de choisir Box<T>, Rc<T> ou RefCell<T> :

  • Rc<T> permet d'avoir plusieurs propriétaires pour une même donnée ; Box<T> et RefCell<T> n'ont qu'un seul propriétaire.
  • Box<T> permet des emprunts immuables ou mutables à la compilation ; Rc<T> permet uniquement des emprunts immuables, vérifiés à la compilation ; RefCell<T> permet des emprunts immuables ou mutables, vérifiés à l'exécution.
  • Comme RefCell<T> permet des emprunts mutables, vérifiés à l'exécution, vous pouvez muter la valeur à l'intérieur du RefCell<T> même si le RefCell<T> est immuable.

Modifer une valeur à l'intérieur d'une valeur immuable est ce qu'on appelle le motif de mutabilité interne. Découvrons une situation pour laquelle la mutabilité interne s'avère utile, puis examinons comment cela est rendu possible.

Mutabilité interne : un emprunt mutable d'une valeur immuable

Une des conséquences des règles d'emprunt est que lorsque vous avez une valeur immuable, vous ne pouvez pas emprunter sa mutabilité. Par exemple, ce code ne va pas se compiler :

fn main() {
    let x = 5;
    let y = &mut x;
}

Si vous essayez de compiler ce code, vous allez obtenir l'erreur suivante :

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` due to previous error

Cependant, il existe des situations pour lesquelles il serait utile qu'une valeur puisse se modifier elle-même dans ses propres méthodes mais qui semble être immuable pour le reste du code. Le code à l'extérieur des méthodes de la valeur n'est pas capable de modifier la valeur. L'utilisation de RefCell<T> est une manière de pouvoir procéder à des mutations internes. Mais RefCell<T> ne contourne pas complètement les règles d'emprunt : le vérificateur d'emprunt du compilateur permet cette mutabilité interne, et les règles d'emprunt sont plutôt vérifiées à l'exécution. Si vous violez les règles, vous allez provoquer un panic! plutôt que d'avoir une erreur de compilation.

Voyons un exemple pratique dans lequel nous pouvons utiliser RefCell<T> pour modifier une valeur immuable et voir en quoi cela est utile.

Un cas d'utilisation de la mutabilité interne : le mock object

Un double de test est un concept général de programmation consistant à utiliser un type à la place d'un autre pendant des tests. Un mock object est un type particulier de double de test qui enregistre ce qui se passe lors d'un test afin que vous puissiez vérifier que les actions se sont passées correctement.

Rust n'a pas d'objets au sens où l'entendent les autres langages qui en ont, et Rust n'offre pas non plus de fonctionnalité de mock object dans la bibliothèque standard comme le font d'autres langages. Cependant, vous pouvez très bien créer une structure qui va répondre aux mêmes besoins qu'un mock object.

Voici le scénario que nous allons tester : nous allons créer une bibliothèque qui surveillera la proximité d'une valeur par rapport à une valeur maximale et enverra des messages en fonction de cette limite. Par exemple, cette bibliothèque peut être utilisée pour suivre le quota d'un utilisateur afin de suivre le nombre d'appels aux API qu'il est autorisé à faire.

Notre bibliothèque fournira uniquement la fonctionnalité de suivi en fonction de la proximité d'une valeur avec la maximale et définiera quels seront les messages associés. Les applications qui utiliseront notre bibliothèque devront fournir un mécanisme pour envoyer les messages : l'application peut afficher le message dans l'application, l'envoyer par email, l'envoyer par SMS ou autre chose. La bibliothèque n'a pas à se charger de ce détail. Tout ce que ce mécanisme doit faire est d'implémenter un trait Messager que nous allons fournir. L'encart 15-20 propose le code pour cette bibliothèque :

Fichier : src/lib.rs

pub trait Messager {
    fn envoyer(&self, msg: &str);
}

pub struct TraqueurDeLimite<'a, T: Messager> {
    messager: &'a T,
    valeur: usize,
    max: usize,
}

impl<'a, T> TraqueurDeLimite<'a, T>
where
    T: Messager,
{
    pub fn new(messager: &T, max: usize) -> TraqueurDeLimite<T> {
        TraqueurDeLimite {
            messager,
            valeur: 0,
            max,
        }
    }

    pub fn set_valeur(&mut self, valeur: usize) {
        self.valeur = valeur;

        let pourcentage_du_maximum = self.valeur as f64 / self.max as f64;

        if pourcentage_du_maximum >= 1.0 {
            self.messager.envoyer("Erreur : vous avez dépassé votre quota !");
        } else if pourcentage_du_maximum >= 0.9 {
            self.messager
                .envoyer("Avertissement urgent : vous avez utilisé 90% de votre quota !");
        } else if pourcentage_du_maximum >= 0.75 {
            self.messager
                .envoyer("Avertissement : vous avez utilisé 75% de votre quota !");
        }
    }
}

Encart 15-20 : une bibliothèque qui suit la proximité d'une valeur avec une valeur maximale et avertit lorsque cette valeur atteint un certain seuil

La partie la plus importante de ce code est celle où le trait Messager a une méthode qui fait appel à envoyer en prenant une référence immuable à self ainsi que le texte du message. Ce trait est l'interface que notre mock object doit implémenter afin que le mock puisse être utilisé de la même manière que l'objet réel. L'autre partie importante est lorsque nous souhaitons tester le comportement de la méthode set_valeur sur le TraqueurDeLimite. Nous pouvons changer ce que nous envoyons dans le paramètre valeur, mais set_valeur ne nous retourne rien qui nous permettrait de le vérifier. Nous voulons pouvoir dire que si nous créons un TraqueurDeLimite avec quelque chose qui implémente le trait Messager et une valeur précise pour max, lorsque nous passons différents nombres pour valeur, le messager reçoit bien l'instruction d'envoyer les messages correspondants.

Nous avons besoin d'un mock object qui, au lieu d'envoyer un email ou un SMS lorsque nous faisons appel à envoyer, va seulement enregistrer les messages qu'on lui demande d'envoyer. Nous pouvons créer une nouvelle instance du mock object, créer un TraqueurDeLimite qui utilise le mock object, faire appel à la méthode set_value sur le TraqueurDeLimite et ensuite vérifier que le mock object a bien les messages que nous attendions. L'encart 15-21 montre une tentative d'implémentation d'un mock object qui fait ceci, mais le vérificateur d'emprunt ne nous autorise pas à le faire :

Fichier : src/lib.rs

pub trait Messager {
    fn envoyer(&self, msg: &str);
}

pub struct TraqueurDeLimite<'a, T: Messager> {
    messager: &'a T,
    valeur: usize,
    max: usize,
}

impl<'a, T> TraqueurDeLimite<'a, T>
where
    T: Messager,
{
    pub fn new(messager: &T, max: usize) -> TraqueurDeLimite<T> {
        TraqueurDeLimite {
            messager,
            valeur: 0,
            max,
        }
    }

    pub fn set_valeur(&mut self, valeur: usize) {
        self.valeur = valeur;

        let pourcentage_du_maximum = self.valeur as f64 / self.max as f64;

        if pourcentage_du_maximum >= 1.0 {
            self.messager.envoyer("Erreur : vous avez dépassé votre quota !");
        } else if pourcentage_du_maximum >= 0.9 {
            self.messager
                .envoyer("Avertissement urgent : vous avez utilisé 90% de votre quota !");
        } else if pourcentage_du_maximum >= 0.75 {
            self.messager
                .envoyer("Avertissement : vous avez utilisé 75% de votre quota !");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MessagerMock {
        messages_envoyes: Vec<String>,
    }

    impl MessagerMock {
        fn new() -> MessagerMock {
            MessagerMock {
                messages_envoyes: vec![],
            }
        }
    }

    impl Messager for MessagerMock {
        fn envoyer(&self, message: &str) {
            self.messages_envoyes.push(String::from(message));
        }
    }

    #[test]
    fn envoi_d_un_message_d_avertissement_superieur_a_75_pourcent() {
        let messager_mock = MessagerMock::new();
        let mut traqueur = TraqueurDeLimite::new(&messager_mock, 100);

        traqueur.set_valeur(80);

        assert_eq!(messager_mock.messages_envoyes.len(), 1);
    }
}

Encart 15-21 : une tentative d'implémentation d'un MessagerMock qui n'est pas autorisée par le vérificateur d'emprunt

Ce code de test définit une structure MessagerMock qui a un champ messages_envoyes qui est un Vec de valeurs String, afin d'y enregistrer les messages qui lui sont envoyés. Nous définissons également une fonction associée new pour faciliter la création de valeurs MessagerMock qui commencent avec une liste vide de messages. Nous implémentons ensuite le trait Messager sur MessagerMock afin de donner un MessagerMock à un TraqueurDeLimite. Dans la définition de la méthode envoyer, nous prenons le message envoyé en paramètre et nous le stockons dans la liste messages_envoyes du MessagerMock.

Dans le test, nous vérifions ce qui se passe lorsque le TraqueurDeLimite doit atteindre une valeur qui est supérieure à 75 pourcent de la valeur max. D'abord, nous créons un nouveau MessagerMock, qui va démarrer avec une liste vide de messages. Ensuite, nous créons un nouveau TraqueurDeLimite et nous lui donnons une référence vers ce MessagerMock et une valeur max de 100. Nous appelons la méthode set_valeur sur le TraqueurDeLimite avec une valeur de 80, qui est plus grande que 75 pourcents de 100. Enfin, nous vérifions que la liste de messages qu'a enregistrée le MessagerMock contient bien désormais un message.

Cependant, il reste un problème avec ce test, problème qui est montré ci-dessous :

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.messages_envoyes` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
2  |     fn envoyer(&self, message: &str);
   |                ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 |             self.messages_envoyes.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed

Nous ne pouvons pas modifier le MessagerMock pour enregistrer les messages, car la méthode envoyer utilise une référence immuable à self. Nous ne pouvons pas non plus suivre la suggestion du texte d'erreur pour utiliser &mut self à la place, car ensuite la signature de envoyer ne va pas correspondre à la signature de la définition du trait Messager (essayez et vous constaterez le message d'erreur que vous obtiendrez).

C'est une situation dans laquelle la mutabilité interne peut nous aider ! Nous allons stocker messages_envoyes dans une RefCell<T>, et ensuite la méthode envoyer pourra modifier messages_envoyes pour stocker les messages que nous avons avons vus. L'encart 15-22 montre à quoi cela peut ressembler :

Fichier : src/lib.rs

pub trait Messager {
    fn envoyer(&self, msg: &str);
}

pub struct TraqueurDeLimite<'a, T: Messager> {
    messager: &'a T,
    valeur: usize,
    max: usize,
}

impl<'a, T> TraqueurDeLimite<'a, T>
where
    T: Messager,
{
    pub fn new(messager: &T, max: usize) -> TraqueurDeLimite<T> {
        TraqueurDeLimite {
            messager,
            valeur: 0,
            max,
        }
    }

    pub fn set_valeur(&mut self, valeur: usize) {
        self.valeur = valeur;

        let pourcentage_du_maximum = self.valeur as f64 / self.max as f64;

        if pourcentage_du_maximum >= 1.0 {
            self.messager.envoyer("Erreur : vous avez dépassé votre quota !");
        } else if pourcentage_du_maximum >= 0.9 {
            self.messager
                .envoyer("Avertissement urgent : vous avez utilisé 90% de votre quota !");
        } else if pourcentage_du_maximum >= 0.75 {
            self.messager
                .envoyer("Avertissement : vous avez utilisé 75% de votre quota !");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MessagerMock {
        messages_envoyes: RefCell<Vec<String>>,
    }

    impl MessagerMock {
        fn new() -> MessagerMock {
            MessagerMock {
                messages_envoyes: RefCell::new(vec![]),
            }
        }
    }

    impl Messager for MessagerMock {
        fn envoyer(&self, message: &str) {
            self.messages_envoyes.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn envoi_d_un_message_d_avertissement_superieur_a_75_pourcent() {
        // -- partie masquée ici --
        let messager_mock = MessagerMock::new();
        let mut traqueur = TraqueurDeLimite::new(&messager_mock, 100);

        traqueur.set_valeur(80);

        assert_eq!(messager_mock.messages_envoyes.borrow().len(), 1);
    }
}

Encart 15-22 : utilisation du RefCell<T> pour muter une valeur interne que les valeurs externes considèrent comme immuable

Le champ messages_envoyes est maintenant du type RefCell<Vec<String>> au lieu de Vec<String>. Dans la fonction new, nous créons une nouvelle instance de RefCell<Vec<String>> autour du vecteur vide.

En ce qui concerne l'implémentation de la méthode envoyer, le premier paramètre est toujours un emprunt immuable de self, ce qui correspond à la définition du trait. Nous appelons la méthode borrow_mut sur le RefCell<Vec<String>> présent dans self.messages_envoyes pour obtenir une référence mutable vers la valeur présente dans le RefCell<Vec<String>>, qui correspond au vecteur. Ensuite, nous appelons push sur la référence mutable vers le vecteur pour enregistrer le message envoyé pendant le test.

Le dernier changement que nous devons appliquer se trouve dans la vérification : pour savoir combien d'éléments sont présents dans le vecteur, nous faisons appel à borrow de RefCell<Vec<String>> pour obtenir une référence immuable vers le vecteur.

Maintenant que vous avez appris à utiliser RefCell<T>, regardons comment il fonctionne !

Suivre les emprunts à l'exécution avec RefCell<T>

Lorsque nous créons des références immuables et mutables, nous utilisons respectivement les syntaxes & et &mut. Avec RefCell<T>, nous utilisons les méthodes borrow et borrow_mut, qui font partie de l'API stable de RefCell<T>. La méthode borrow retourne un pointeur intelligent du type Ref<T> et borrow_mut retourne un pointeur intelligent du type RefMut<T>. Les deux implémentent Deref, donc nous pouvons les considérer comme des références classiques.

Le RefCell<T> suit combien de pointeurs intelligents Ref<T> et RefMut<T> sont actuellement actifs. A chaque fois que nous faisons appel à borrow, le RefCell<T> augmente son compteur du nombre d'emprunts immuables qui existent. Lorsqu'une valeur Ref<T> sort de la portée, le compteur d'emprunts immuables est décrémenté de un. A tout moment RefCell<T> nous permet d'avoir plusieurs emprunts immuables ou bien un seul emprunt mutable, tout comme le font les règles d'emprunt au moment de la compilation.

Si nous ne respectons pas ces règles, l'implémentation de RefCell<T> va paniquer à l'exécution plutôt que de provoquer une erreur de compilation comme nous l'aurions eu en utilisant des références classiques. L'encart 15-23 nous montre une modification apportée à l'implémentation de envoyer de l'encart 15-22. Nous essayons délibérément de créer deux emprunts mutables actifs dans la même portée pour montrer que RefCell<T> nous empêche de faire ceci à l'exécution.

Fichier : src/lib.rs

pub trait Messager {
    fn envoyer(&self, msg: &str);
}

pub struct TraqueurDeLimite<'a, T: Messager> {
    messager: &'a T,
    valeur: usize,
    max: usize,
}

impl<'a, T> TraqueurDeLimite<'a, T>
where
    T: Messager,
{
    pub fn new(messager: &T, max: usize) -> TraqueurDeLimite<T> {
        TraqueurDeLimite {
            messager,
            valeur: 0,
            max,
        }
    }

    pub fn set_valeur(&mut self, valeur: usize) {
        self.valeur = valeur;

        let pourcentage_du_maximum = self.valeur as f64 / self.max as f64;

        if pourcentage_du_maximum >= 1.0 {
            self.messager.envoyer("Erreur : vous avez dépassé votre quota !");
        } else if pourcentage_du_maximum >= 0.9 {
            self.messager
                .envoyer("Avertissement urgent : vous avez utilisé 90% de votre quota !");
        } else if pourcentage_du_maximum >= 0.75 {
            self.messager
                .envoyer("Avertissement : vous avez utilisé 75% de votre quota !");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MessagerMock {
        messages_envoyes: RefCell<Vec<String>>,
    }

    impl MessagerMock {
        fn new() -> MessagerMock {
            MessagerMock {
                messages_envoyes: RefCell::new(vec![]),
            }
        }
    }

    impl Messager for MessagerMock {
        fn envoyer(&self, message: &str) {
            let mut premier_emprunt = self.messages_envoyes.borrow_mut();
            let mut second_emprunt = self.messages_envoyes.borrow_mut();

            premier_emprunt.push(String::from(message));
            second_emprunt.push(String::from(message));
        }
    }

    #[test]
    fn envoi_d_un_message_d_avertissement_superieur_a_75_pourcent() {
        let messager_mock = MessagerMock::new();
        let mut traqueur = TraqueurDeLimite::new(&messager_mock, 100);

        traqueur.set_valeur(80);

        assert_eq!(messager_mock.messages_envoyes.borrow().len(), 1);
    }
}

Encart 15-23 : création de deux références mutables dans la même portée pour voir si RefCell<T> va paniquer

Nous créons une variable premier_emprunt pour le pointeur intelligent RefMut<T> retourné par borrow_mut. Ensuite nous créons un autre emprunt de la même manière, qui s'appelle second_emprunt. Cela fait deux références mutables dans la même portée, ce qui n'est pas autorisé. Lorsque nous lançons les tests sur notre bibliothèque, le code de l'encart 15-23 va se compiler sans erreur, mais les tests vont échouer :

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::envoi_d_un_message_d_avertissement_superieur_a_75_pourcent ... FAILED

failures:

---- tests::envoi_d_un_message_d_avertissement_superieur_a_75_pourcent stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::envoi_d_un_message_d_avertissement_superieur_a_75_pourcent

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Remarquez que le code a paniqué avec le message already borrowed: BorrowMutError (NdT : déjà emprunté). C'est ainsi que RefCell<T> gère les violations des règles d'emprunt à l'exécution.

La détection des erreurs d'emprunt à l'exécution plutôt qu'à la compilation signifie que vous pourriez découvrir une erreur dans votre code plus tard dans le processus de développement et peut-être même pas avant que votre code ne soit déployé en production. De plus, votre code va subir une petite perte de performances à l'exécution en raison du contrôle des emprunts à l'exécution plutôt qu'à la compilation. Cependant, l'utilisation de RefCell<T> rend possible l'écriture d'un mock object qui peut se modifier lui-même afin d'enregistrer les messages qu'il a vu passer alors que vous l'utilisez dans un contexte où seules les valeurs immuables sont permises. Vous pouvez utiliser RefCell<T> malgré ses inconvénients pour obtenir plus de fonctionnalités que celles qu'offre une référence classique.

Permettre plusieurs propriétaires de données mutables en combinant Rc<T> et RefCell<T>

Il est courant d'utiliser RefCell<T> en tandem avec Rc<T>. Rappelez-vous que Rc<T> vous permet d'avoir plusieurs propriétaires d'une même donnée, mais qu'il ne vous donne qu'un seul accès immuable à cette donnée. Si vous avez un Rc<T> qui contient un RefCell<T>, vous pouvez obtenir une valeur qui peut avoir plusieurs propriétaires et que vous pouvez modifier !

Souvenez-vous de l'exemple de la liste de construction de l'encart 15-18 où nous avions utilisé Rc<T> pour permettre à plusieurs listes de se partager la possession d'une autre liste. Comme Rc<T> stocke seulement des valeurs immuables, nous ne pouvons changer aucune valeur dans la liste une fois que nous l'avons créée. Ajoutons un RefCell<T> pour pouvoir changer les valeurs dans les listes. L'encart 15-24 nous montre ceci en ajoutant un RefCell<T> dans la définition de Cons, nous pouvons ainsi modifier les valeurs stockées dans n'importe quelle liste :

Fichier : src/main.rs

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let valeur = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&valeur), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *valeur.borrow_mut() += 10;

    println!("a après les opérations = {:?}", a);
    println!("b après les opérations = {:?}", b);
    println!("c après les opérations = {:?}", c);
}

Encart 15-24 : utilisation de Rc<RefCell<i32>> pour créer une List que nous pouvons modifier

Nous créons une valeur qui est une instance de Rc<RefCell<i32>> et nous la stockons dans une variable valeur afin que nous puissions y avoir accès plus tard. Ensuite, nous créons une List dans a avec une variante de Cons qui utilise valeur. Nous devons utiliser clone sur valeur afin que a et valeur soient toutes les deux propriétaires de la valeur interne 5 plutôt que d'avoir à transférer la possession de valeur à a ou avoir a qui emprunte valeur.

Nous insérons la liste a dans un Rc<T> pour que, lorsque nous créons b et c, elles puissent toutes les deux utiliser a, ce que nous avions déjà fait dans l'encart 15-18.

Après avoir créé les listes dans a, b, et c, nous ajoutons 10 à la valeur dans valeur. Nous faisons cela en appelant borrow_mut sur valeur, ce qui utilise la fonctionnalité de déréférencement automatique que nous avons vue au chapitre 5 (voir la section “Où est l'opérateur -> ?”) pour déréférencer le Rc<T> dans la valeur interne RefCell<T>. La méthode borrow_mut retourne un pointeur intelligent RefMut<T>, et nous utilisons l'opérateur de déréférencement sur lui pour changer sa valeur interne.

Lorsque nous affichons a, b et c, nous pouvons constater qu'elles ont toutes la valeur modifiée de 15 au lieu de 5 :

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a après les opérations = Cons(RefCell { value: 15 }, Nil)
b après les opérations = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c après les opérations = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

Cette technique est plutôt ingénieuse ! En utilisant RefCell<T>, nous avons une valeur List qui est immuable de l'extérieur. Mais nous pouvons utiliser les méthodes de RefCell<T> qui nous donne accès à sa mutabilité interne afin que nous puissions modifier notre donnée lorsque nous en avons besoin. Les vérifications des règles d'emprunt à l'exécution nous protègent des accès concurrents, et il est parfois intéressant de sacrifier un peu de vitesse pour cette flexibilité dans nos structures de données.

La bibliothèque standard a d'autres types qui fournissent de la mutabilité interne, comme Cell<T>, qui est similaire sauf qu'au lieu de fournir des références à la valeur interne, la valeur est copiée à l'intérieur et à l'extérieur du Cell<T>. Il existe aussi Mutex<T> qui offre de la mutabilité interne qui est sécurisée pour une utilisation partagée entre plusieures tâches ; nous allons voir son utilisation au chapitre 16. Plongez-vous dans la documentation de la bibliothèque standard pour plus de détails entre ces différents types.