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. Du code unsafe indique au compilateur que nous vérifions les règles manuellement au lieu de nous reposer sur le compilateur pour les vérifier ; nous verrons plus en détail le code unsafe au chapitre 20.
Nous pouvons utiliser des types qui utilisent le motif de mutabilité interne seulement lorsque nous pouvons être sûrs 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
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 :
- À un instant donné, 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 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, qui auraient été interdits par les vérifications effectuées à 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>etRefCell<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 duRefCell<T>même si leRefCell<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.
Utilisation de la mutabilité interne
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
|
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 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.
Tests à l’aide d’objets fictifs
Parfois, durant les phases de tests, un programmeur va utiliser un type à la place d’un autre, de manière à observer un comportement particulier et s’assurer qu’il l’implémente correctement. Ce type de remplacement est appelé un double de test. On peut voir cela comme un cascadeur doublant un acteur lors du tournage d’un film, où une personne remplace un acteur pour tourner une scène particulièrement dangereuse. Les doublures de test remplacent d’autres types durant l’exécution des tests. Les objets simulés (NdT : mock objects) sont des types particuliers de doubles de test qui enregistrent 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éfinira 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 directement à l’utilisateur, l’envoyer par courriel, l’envoyer par SMS ou bien faire 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.
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 !");
}
}
}
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, le messager reçoit bien l’instruction d’envoyer les messages correspondants lorsque nous passons différents nombres pour valeur.
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.
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);
}
}
MockMessenger that isn’t allowed by the borrow checkerCe 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
|
58 | self.messages_envoyes.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 envoyer(&self, msg: &str);
3 | }
...
56 | impl Messager for MessagerMock {
57 ~ fn envoyer(&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 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 dans la méthode impl ainsi que dans la définition du trait. Nous ne voulons pas changer le trait Messenger uniquement dans le but de tester. À la place, nous devons trouver un moyen de faire en sorte que notre code de test fonctionne correctement avec la configuration existante.
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.
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);
}
}
RefCell<T> to mutate an inner value while the outer value is considered immutableLe 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 !
Suivi des emprunts à l’exécution
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. À 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 1. À 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.
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);
}
}
RefCell<T> will panicNous 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` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (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 'tests::envoi_d_un_message_d_avertissement_superieur_a_75_pourcent' panicked at src/lib.rs:60:53:
RefCell already borrowed
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.
Le choix de la détection des erreurs d’emprunt à l’exécution plutôt qu’à la compilation, comme nous venons de le faire ici, signifie que vous pourriez potentiellement découvrir des erreurs 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
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 sa capacité 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.
#[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:?}");
}
Rc<RefCell<i32>> to create a List that we can mutateNous 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 dans la section “Où est l’opérateur -> ?” du chapitre 5 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` profile [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 donnent 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. Notez que RefCell<T> ne fonctionne pas pour du code avec plusieurs fils d’exécution concurrents ! Mutex<T> est la version compatible avec la concurrence de RefCell<T>, nous aborderons le sujet des Mutex<T> dans le chapitre 16.