Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RefCell<T> et le patron de mutabilité intérieure

La mutabilité intérieure est un patron de conception en Rust qui vous permet de modifier des données même lorsqu’il existe des références immuables vers ces données ; normalement, cette action est interdite par les règles d’emprunt. Pour modifier des données, le patron utilise du code unsafe à l’intérieur d’une structure de données pour contourner les règles habituelles de Rust qui régissent la mutation et l’emprunt. Le code unsafe indique au compilateur que nous vérifions les règles manuellement au lieu de compter sur le compilateur pour les vérifier à notre place ; nous aborderons le code unsafe plus en détail au chapitre 20.

Nous ne pouvons utiliser les types qui emploient le patron de mutabilité intérieure que lorsque nous pouvons nous assurer que les règles d’emprunt seront respectées à l’exécution, même si le compilateur ne peut pas le garantir. Le code unsafe impliqué est alors enveloppé dans une API sûre, et le type externe reste immuable.

Explorons ce concept en examinant le type RefCell<T> qui suit le patron de mutabilité intérieure.

Appliquer les règles d’emprunt à l’exécution

Contrairement à Rc<T>, le type RefCell<T> représente une possession unique des données qu’il contient. Alors, qu’est-ce qui rend RefCell<T> différent d’un type comme Box<T> ? Rappelez-vous les règles d’emprunt que vous avez apprises au chapitre 4 : - À tout moment, vous pouvez avoir soit une référence mutable, soit un nombre quelconque de références immuables (mais pas les deux). - Les références doivent toujours être valides.

  • À tout moment, vous pouvez avoir soit une référence mutable, soit un nombre quelconque de références immuables (mais pas les deux).
  • Les références doivent toujours être valides.

Avec les références et Box<T>, les invariants des règles d’emprunt sont appliqués à la compilation. Avec RefCell<T>, ces invariants sont appliqués à l’exécution. Avec les références, si vous enfreignez ces règles, vous obtiendrez une erreur de compilation. Avec RefCell<T>, si vous enfreignez ces règles, votre programme paniquera et se terminera.

Les avantages de vérifier les règles d’emprunt à la compilation sont que les erreurs seront détectées plus tôt dans le processus de développement, et qu’il n’y à aucun impact sur les performances à l’exécution car toute l’analyse est effectuée au préalable. Pour ces raisons, vérifier les règles d’emprunt à la compilation est le meilleur choix dans la majorité des cas, c’est pourquoi c’est le comportement par défaut de Rust.

L’avantage de vérifier les règles d’emprunt à l’exécution à la place est que certains scénarios sûrs en mémoire sont alors autorisés, là où ils auraient été interdits par les vérifications à la compilation. L’analyse statique, comme celle du compilateur Rust, est intrinsèquement conservatrice. Certaines propriétés du code sont impossibles à détecter en analysant le code : l’exemple le plus célèbre est le problème de l’arrêt, qui dépasse le cadre de ce livre mais constitue un sujet de recherche intéressant.

Comme certaines analyses sont impossibles, si le compilateur Rust ne peut pas être sûr que le code respecte les règles de possession, il pourrait rejeter un programme correct ; en ce sens, il est conservateur. Si Rust acceptait un programme incorrect, les utilisateurs ne pourraient pas faire confiance aux garanties que Rust offre. Cependant, si Rust rejette un programme correct, le programmeur sera gêné, mais rien de catastrophique ne peut se produire. Le type RefCell<T> est utile quand vous êtes sûr que votre code suit les règles d’emprunt mais que le compilateur est incapable de le comprendre et de le garantir.

De manière similaire à Rc<T>, RefCell<T> est uniquement destiné aux scénarios mono-thread et vous donnera une erreur de compilation si vous essayez de l’utiliser dans un contexte multi-thread. Nous parlerons de comment obtenir la fonctionnalité de RefCell<T> dans un programme multi-thread au chapitre 16.

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

  • Rc<T> permet plusieurs propriétaires des mêmes données ; Box<T> et RefCell<T> ont un seul propriétaire.
  • Box<T> autorise des emprunts immuables ou mutables vérifiés à la compilation ; Rc<T> autorise uniquement des emprunts immuables vérifiés à la compilation ; RefCell<T> autorise des emprunts immuables ou mutables vérifiés à l’exécution.
  • Parce que RefCell<T> autorise des emprunts mutables vérifiés à l’exécution, vous pouvez muter la valeur à l’intérieur du RefCell<T> même lorsque le RefCell<T> est immuable.

Modifier la valeur à l’intérieur d’une valeur immuable est le patron de mutabilité intérieure. Examinons une situation dans laquelle la mutabilité intérieure est utile et voyons comment c’est possible.

Utiliser la mutabilité intérieure

Une conséquence des règles d’emprunt est que quand vous avez une valeur immuable, vous ne pouvez pas l’emprunter de manière mutable. Par exemple, ce code ne compilera pas : rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch15-smart-pointers/no-listing-01-cant-borrow-immutable-as-mutable/src/main.rs}}

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

Si vous tentiez de compiler ce code, vous obtiendriez l’erreur suivante : console {{#include ../listings/ch15-smart-pointers/no-listing-01-cant-borrow-immutable-as-mutable/output.txt}}

$ 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
  |
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut x = 5;
  |         +++

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

Cependant, il existe des situations dans lesquelles il serait utile qu’une valeur se modifié elle-même dans ses méthodes mais apparaisse immuable au code extérieur. Le code en dehors des méthodes de la valeur ne pourrait pas modifier la valeur. Utiliser RefCell<T> est un moyen d’obtenir la capacité d’avoir une mutabilité intérieure, mais RefCell<T> ne contourne pas complètement les règles d’emprunt : le vérificateur d’emprunts dans le compilateur autorise cette mutabilité intérieure, et les règles d’emprunt sont vérifiées à l’exécution à la place. Si vous violez les règles, vous obtiendrez un panic! au lieu d’une erreur de compilation.

Travaillons sur un exemple pratique où nous pouvons utiliser RefCell<T> pour modifier une valeur immuable et voir pourquoi c’est utile.

Tester avec des objets simulés

Parfois, lors des tests, un programmeur utilise un type à la place d’un autre type, afin d’observer un comportement particulier et de vérifier qu’il est correctement implémenté. Ce type de substitution s’appelle un doublure de test (test double). Pensez-y comme une doublure cascade au cinéma, où une personne se substitue à un acteur pour réaliser une scène particulièrement périlleuse. Les doublures de test remplacent d’autres types lorsque nous exécutons des tests. Les objets simulés (mock objects) sont des types spécifiques de doublures de test qui enregistrent ce qui se passe pendant un test afin que vous puissiez vérifier que les actions correctes ont eu lieu.

Rust n’a pas d’objets au même sens que d’autres langages, et Rust n’a pas de fonctionnalité d’objets simulés intégrée dans la bibliothèque standard comme le font certains autres langages. Cependant, vous pouvez tout à fait créer une struct qui servira les mêmes objectifs qu’un objet simulé.

Voici le scénario que nous allons tester : nous allons créer une bibliothèque qui suit une valeur par rapport à une valeur maximale et envoie des messages en fonction de la proximité de la valeur courante par rapport à la valeur maximale. Cette bibliothèque pourrait être utilisée pour suivre le quota d’un utilisateur pour le nombre d’appels API qu’il est autorisé à effectuer, par exemple.

Notre bibliothèque ne fournira que la fonctionnalité de suivi de la proximité d’une valeur par rapport au maximum et les messages à envoyer à quels moments. Les applications qui utilisent notre bibliothèque devront fournir le mécanisme d’envoi des messages : l’application pourrait afficher le message directement à l’utilisateur, envoyer un email, envoyer un SMS, ou faire autre chose. La bibliothèque n’a pas besoin de connaître ce détail. Tout ce dont elle a besoin est quelque chose qui implémente un trait que nous fournirons, appelé Messenger. L’encart 15-20 montre le code de la bibliothèque.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}
Listing 15-20: A library to keep track of how close a value is to a maximum value and warn when the value is at certain levels

Une partie importante de ce code est que le trait Messenger à une méthode appelée send qui prend une référence immuable vers self et le texte du message. Ce trait est l’interface que notre objet simulé doit implémenter pour que le simulacre puisse être utilisé de la même manière qu’un vrai objet. L’autre partie importante est que nous voulons tester le comportement de la méthode set_value sur le LimitTracker. Nous pouvons changer ce que nous passons pour le paramètre value, mais set_value ne retourné rien sur quoi faire des assertions. Nous voulons pouvoir dire que si nous créons un LimitTracker avec quelque chose qui implémente le trait Messenger et une valeur particulière pour max, le messager reçoit l’instruction d’envoyer les messages appropriés quand nous passons différents nombres pour value.

Nous avons besoin d’un objet simulé qui, au lieu d’envoyer un email ou un SMS quand nous appelons send, ne fera que garder trace des messages qu’on lui demande d’envoyer. Nous pouvons créer une nouvelle instance de l’objet simulé, créer un LimitTracker qui utilise l’objet simulé, appeler la méthode set_value sur LimitTracker, puis vérifier que l’objet simulé contient les messages attendus. L’encart 15-21 montre une tentative d’implémentation d’un objet simulé pour faire exactement cela, mais le vérificateur d’emprunts ne le permettra pas.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

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

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}
Listing 15-21: An attempt to implement a MockMessenger that isn’t allowed by the borrow checker

Ce code de test définit une struct MockMessenger qui à un champ sent_messages avec un Vec de valeurs String pour garder trace des messages qu’on lui demande d’envoyer. Nous définissons aussi une fonction associée new pour faciliter la création de nouvelles valeurs MockMessenger qui commencent avec une liste vide de messages. Nous implémentons ensuite le trait Messenger pour MockMessenger afin de pouvoir donner un MockMessenger à un LimitTracker. Dans la définition de la méthode send, nous prenons le message passé en paramètre et le stockons dans la liste sent_messages du MockMessenger.

Dans le test, nous testons ce qui se passe quand on demande au LimitTracker de définir value à quelque chose qui représente plus de 75 pour cent de la valeur max. D’abord, nous créons un nouveau MockMessenger, qui commencera avec une liste vide de messages. Ensuite, nous créons un nouveau LimitTracker et lui donnons une référence vers le nouveau MockMessenger et une valeur max de 100. Nous appelons la méthode set_value sur le LimitTracker avec une valeur de 80, qui est plus de 75 pour cent de 100. Puis, nous vérifions que la liste de messages que le MockMessenger suit devrait maintenant contenir un message.

Cependant, il y à un problème avec ce test, comme montré ici : console {{#include ../listings/ch15-smart-pointers/listing-15-21/output.txt}}

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
 2 ~     fn send(&mut self, msg: &str);
 3 | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

Nous ne pouvons pas modifier le MockMessenger pour garder trace des messages, car la méthode send prend une référence immuable vers self. Nous ne pouvons pas non plus suivre la suggestion du texte d’erreur d’utiliser &mut self à la fois dans la méthode impl et la définition du trait. Nous ne voulons pas changer le trait Messenger uniquement pour les besoins des tests. Au lieu de cela, nous devons trouver un moyen de faire fonctionner correctement notre code de test avec notre conception existante.

C’est une situation dans laquelle la mutabilité intérieure peut aider ! Nous allons stocker les sent_messages dans un RefCell<T>, et alors la méthode send pourra modifier sent_messages pour stocker les messages que nous avons vus. L’encart 15-22 montre à quoi cela ressemble.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

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

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

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

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}
Listing 15-22: Using RefCell<T> to mutate an inner value while the outer value is considered immutable

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

Pour l’implémentation de la méthode send, le premier paramètre est toujours un emprunt immuable de self, ce qui correspond à la définition du trait. Nous appelons borrow_mut sur le RefCell<Vec<String>> dans self.sent_messages pour obtenir une référence mutable vers la valeur à l’intérieur du RefCell<Vec<String>>, qui est le vecteur. Ensuite, nous pouvons appeler push sur la référence mutable vers le vecteur pour garder trace des messages envoyés pendant le test.

Le dernier changement que nous devons faire est dans l’assertion : pour voir combien d’éléments se trouvent dans le vecteur intérieur, nous appelons borrow sur le RefCell<Vec<String>> pour obtenir une référence immuable vers le vecteur.

Maintenant que vous avez vu comment utiliser RefCell<T>, examinons comment il fonctionne !

Suivre les emprunts à l’exécution

Lors de la création de références immuables et mutables, nous utilisons respectivement la syntaxe & et &mut. Avec RefCell<T>, nous utilisons les méthodes borrow et borrow_mut, qui font partie de l’API sûre de RefCell<T>. La méthode borrow retourné le type de pointeur intelligent Ref<T>, et borrow_mut retourné le type de pointeur intelligent RefMut<T>. Les deux types implémentent Deref, nous pouvons donc les traiter comme des références classiques.

Le RefCell<T> suit le nombre de pointeurs intelligents Ref<T> et RefMut<T> actuellement actifs. Chaque fois que nous appelons borrow, le RefCell<T> augmente son compteur d’emprunts immuables actifs. Quand une valeur Ref<T> sort de la portée, le compteur d’emprunts immuables diminue de 1. Tout comme les règles d’emprunt à la compilation, RefCell<T> nous permet d’avoir de nombreux emprunts immuables ou un seul emprunt mutable à tout moment.

Si nous essayons de violer ces règles, plutôt que d’obtenir une erreur de compilation comme ce serait le cas avec les références, l’implémentation de RefCell<T> paniquera à l’exécution. L’encart 15-23 montre une modification de l’implémentation de send 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 illustrer que RefCell<T> nous empêche de le faire à l’exécution.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

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

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

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

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}
Listing 15-23: Creating two mutable references in the same scope to see that RefCell<T> will panic

Nous créons une variable one_borrow pour le pointeur intelligent RefMut<T> retourné par borrow_mut. Ensuite, nous créons un autre emprunt mutable de la même manière dans la variable two_borrow. Cela crée deux références mutables dans la même portée, ce qui n’est pas autorisé. Quand nous exécutons les tests de notre bibliothèque, le code de l’encart 15-23 compilera sans aucune erreur, mais le test échouera : console {{#include ../listings/ch15-smart-pointers/listing-15-23/output.txt}}

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

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----

thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

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. C’est ainsi que RefCell<T> gère les violations des règles d’emprunt à l’exécution.

Choisir de capturer les erreurs d’emprunt à l’exécution plutôt qu’à la compilation, comme nous l’avons fait ici, signifie que vous trouveriez potentiellement des erreurs dans votre code plus tard dans le processus de développement : peut-être pas avant que votre code ne soit déployé en production. De plus, votre code subirait une légère pénalité de performance à l’exécution du fait du suivi des emprunts à l’exécution plutôt qu’à la compilation. Cependant, utiliser RefCell<T> rend possible l’écriture d’un objet simulé qui peut se modifier lui-même pour garder trace des messages qu’il a vus tout en l’utilisant dans un contexte où seules les valeurs immuables sont autorisées. Vous pouvez utiliser RefCell<T> malgré ses compromis pour obtenir plus de fonctionnalités que ce que les références classiques fournissent.

Permettre plusieurs propriétaires de données mutables

Une façon courante d’utiliser RefCell<T> est en combinaison avec Rc<T>. Rappelez-vous que Rc<T> vous permet d’avoir plusieurs propriétaires de certaines données, mais il ne donne qu’un accès immuable à ces données. 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 !

Par exemple, rappelez-vous l’exemple de la liste cons de l’encart 15-18 où nous avons utilisé Rc<T> pour permettre à plusieurs listes de partager la possession d’une autre liste. Comme Rc<T> ne contient que des valeurs immuables, nous ne pouvons pas changer les valeurs de la liste une fois que nous les avons créées. Ajoutons RefCell<T> pour sa capacité à changer les valeurs dans les listes. L’encart 15-24 montre qu’en utilisant un RefCell<T> dans la définition de Cons, nous pouvons modifier la valeur stockée dans toutes les listes.

Filename: 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 value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), 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));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}");
    println!("b after = {b:?}");
    println!("c after = {c:?}");
}
Listing 15-24: Using Rc<RefCell<i32>> to create a List that we can mutate

Nous créons une valeur qui est une instance de Rc<RefCell<i32>> et la stockons dans une variable nommée value afin de pouvoir y accéder directement plus tard. Ensuite, nous créons une List dans a avec une variante Cons qui contient value. Nous devons cloner value pour que a et value aient tous les deux la possession de la valeur intérieure 5 plutôt que de transférer la possession de value vers a ou de faire emprunter a depuis value.

Nous enveloppons la liste a dans un Rc<T> pour que quand nous créons les listes b et c, elles puissent toutes les deux faire référence à a, ce que nous avons fait dans l’encart 15-18.

Après avoir créé les listes dans a, b et c, nous voulons ajouter 10 à la valeur dans value. Nous le faisons en appelant borrow_mut sur value, qui utilise la fonctionnalité de déréférencement automatique dont nous avons discuté dans [“Où est l’opérateur -> ?”][wheres-the—operator] au chapitre 5 pour déréférencer le Rc<T> vers la valeur intérieure RefCell<T>. La méthode borrow_mut retourné un pointeur intelligent RefMut<T>, et nous utilisons l’opérateur de déréférencement dessus pour changer la valeur intérieure.

Quand nous affichons a, b et c, nous pouvons voir qu’ils ont tous la valeur modifiée de 15 plutôt que 5 : console {{#include ../listings/ch15-smart-pointers/listing-15-24/output.txt}}

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

Cette technique est plutôt élégante ! En utilisant RefCell<T>, nous avons une valeur List extérieurement immuable. Mais nous pouvons utiliser les méthodes de RefCell<T> qui fournissent l’accès à sa mutabilité intérieure afin de pouvoir modifier nos données quand nous en avons besoin. Les vérifications à l’exécution des règles d’emprunt nous protègent des courses de données, et il vaut parfois la peine d’échanger un peu de vitesse contre cette flexibilité dans nos structures de données. Notez que RefCell<T> ne fonctionne pas pour le code multi-thread ! Mutex<T> est la version sûre pour les threads de RefCell<T>, et nous aborderons Mutex<T> au chapitre 16.