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>
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.
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 !");
}
}
}
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);
}
}
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);
}
}
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);
}
}
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); }
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.