Rc<T>
, le pointeur intelligent qui compte les références
Dans la majorité des cas, la possession est claire : vous savez exactement quelle variable possède une valeur donnée. Cependant, il existe des cas où une valeur peut être possédée par plusieurs propriétaires. Par exemple, dans des structures de données de graphes, plusieurs extrémités peuvent pointer vers le même noeud, et ce noeud est par conception possédé par toutes les extrémités qui pointent vers lui. Un noeud ne devrait pas être nettoyé, à moins qu'il n'ait plus d'extrémités qui pointent vers lui.
Pour permettre la possession multiple, Rust dispose du type Rc<T>
, qui est une
abréviation pour Reference Counting
(compteur de références). Le type
Rc<T>
assure le suivi du nombre de références vers une valeur, afin de
déterminer si la valeur est toujours utilisée ou non. S'il y a zéro références
vers une valeur, la valeur peut être nettoyée sans qu'aucune référence ne devienne
invalide.
Imaginez que Rc<T>
est comme une télévision dans une salle commune. Lorsqu'une
personne entre pour regarder la télévision, elle l'allume. Une autre entre dans
la salle et regarde la télévision. Lorsque la dernière personne quitte la salle,
elle éteint la télévision car elle n'est plus utilisée. Si quelqu'un éteint la
télévision alors que d'autres continuent à la regarder, cela va provoquer du
chahut !
Nous utilisons le type Rc<T>
lorsque nous souhaitons allouer une donnée sur
le tas pour que plusieurs éléments de notre programme puissent la lire et que
nous ne pouvons pas déterminer au moment de la compilation quel élément cessera
de l'utiliser en dernier. Si nous savions quel élément finirait en dernier,
nous pourrions simplement faire en sorte que cet élément prenne possession de
la donnée, et les règles de possession classiques qui s'appliquent au moment de
la compilation prendraient effet.
Notez que Rc<T>
fonctionne uniquement dans des scénarios à un seul processus.
Lorsque nous verrons la concurrence au chapitre 16, nous verrons comment
procéder au comptage de références dans des programmes multi-processus.
Utiliser Rc<T>
pour partager une donnée
Retournons à notre exemple de liste de construction de l'encart 15-5.
Souvenez-vous que nous l'avons défini en utilisant Box<T>
. Cette fois-ci, nous
allons créer deux listes qui partagent toutes les deux la propriété d'une
troisième liste. Théoriquement, cela ressemblera à l'illustration 15-3 :
Nous allons créer une liste a
qui contient 5
et ensuite 10
. Ensuite, nous
allons créer deux autres listes : b
qui démarre avec 3
et c
qui démarre
avec 4
. Les deux listes b
et c
vont ensuite continuer sur la première
liste a
qui contient déjà 5
et 10
. Autrement dit, les deux listes vont se
partager la première liste contenant 5
et 10
.
Si nous essayons d'implémenter ce scénario en utilisant les définitions de
List
avec Box<T>
, comme dans l'encart 15-17, cela ne va pas fonctionner :
Fichier : src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
Lorsque nous compilons ce code, nous obtenons cette erreur :
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error
Les variantes Cons
prennent possession des données qu'elles obtiennent, donc
lorsque nous avons créé la liste b
, a
a été déplacée dans b
et b
possède
désormais a
. Ensuite, lorsque nous essayons d'utiliser a
à nouveau lorsque
nous créons c
, nous ne sommes pas autorisés à le faire car a
a été déplacé.
Nous pourrions changer la définition de Cons
pour stocker des références à la
place, mais ensuite nous aurions besoin de renseigner des paramètres de durée
de vie. En renseignant les paramètres de durée de vie, nous devrions préciser
que chaque élément dans la liste vivra au moins aussi longtemps que la liste
entière. C'est le cas pour les éléments et les listes dans l'encart 15-17, mais
pas dans tous les cas.
A la place, nous allons changer la définition de List
pour utiliser Rc<T>
à
la place de Box<T>
, comme dans l'encart 15-18. Chaque variante Cons
va
maintenant posséder une valeur et un Rc<T>
pointant sur une List
. Lorsque
nous créons b
, au lieu de prendre possession de a
, nous allons cloner le
Rc<List>
que a
possède, augmentant ainsi le nombre de références de un à
deux et permettant à a
et b
de partager la propriété des données dans
Rc<List>
. Nous allons aussi cloner a
lorsque nous créons c
, augmentant le
nombre de références de deux à trois. Chaque fois que nous appelons Rc::clone
,
le compteur de références des données présentes dans le Rc<List>
va augmenter,
et les données ne seront pas nettoyées tant qu'il n'y aura pas zéro référence
vers elles.
Filename : src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }
Nous devons ajouter une instruction use
pour importer Rc<T>
dans la portée
car il n'est pas présent dans l'étape préliminaire. Dans le main
, nous créons
la liste qui stocke 5
et 10
et la stocke dans une nouvelle Rc<List>
dans
a
. Ensuite, lorsque nous créons b
et c
, nous appelons la fonction
Rc::clone
et nous passons une référence vers le Rc<List>
de a
en argument.
Nous aurions pu appeler a.clone()
plutôt que Rc::clone(&a)
, mais la
convention en Rust est d'utiliser Rc::clone
dans cette situation.
L'implémentation de Rc::clone
ne fait pas une copie profonde de toutes les
données comme le fait la plupart des implémentations de clone
. L'appel à
Rc:clone
augmente uniquement le compteur de références, ce qui ne prend pas
beaucoup de temps. Les copies profondes des données peuvent prendre beaucoup de
temps. En utilisant Rc::clone
pour les compteurs de références, nous pouvons
distinguer visuellement un clonage qui fait une copie profonde d'un clonage qui
augmente uniquement le compteur de références. Lorsque vous enquêtez sur des
problèmes de performances dans le code, vous pouvez ainsi écarter les appels à
Rc::clone
pour ne vous intéresser qu'aux clonages à copie profonde que vous
recherchez probablement.
Cloner une Rc<T>
augmente le compteur de référence
Changeons notre exemple de l'encart 15-18 pour que nous puissions voir le
compteur de références changer au fur et à mesure que nous créons et libérons
des références dans le Rc<List>
présent dans a
.
Dans l'encart 15-19, nous allons changer le main
afin qu'il ait une portée
en son sein autour de c
; ainsi nous pourrons voir comment le compteur de
références change lorsque c
sort de la portée.
Fichier : src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("compteur après la création de a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("compteur après la création de b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("compteur après la création de c = {}", Rc::strong_count(&a)); } println!("compteur après que c est sorti de la portée = {}", Rc::strong_count(&a)); }
A chaque étape du programme où le compteur de références change, nous affichons
le compteur de références, que nous pouvons obtenir en faisant appel à la
fonction Rc::strong_count
. Cette fonction s'appelle strong_count
plutôt que
count
car le type Rc<T>
a aussi un weak_count
; nous verrons à quoi sert
ce weak_count
dans
la dernière section de ce chapitre.
Ce code affiche ceci :
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
compteur après la création de a = 1
compteur après la création de b = 2
compteur après la création de c = 3
compteur après que c est sorti de la portée = 2
Nous pouvons voir clairement que le Rc<List>
dans a
a un compteur de
références initial à 1
; puis à chaque fois que nous appelons clone
, le
compteur augmente de 1. Nous n'avons pas à appeler une fonction pour réduire le
compteur de références, comme nous avons dû le faire avec Rc::clone
pour
augmenter compteur : l'implémentation du trait Drop
réduit le compteur de
références automatiquement lorsqu'une valeur de Rc<T>
sort de la portée.
Ce que nous ne voyons pas dans cet exemple, c'est que lorsque b
et a
sortent de la portée à la fin du main
, le compteur vaut alors 0 et que le
Rc<List>
est nettoyé complètement à ce moment. L'utilisation de Rc<T>
permet à une valeur d'avoir plusieurs propriétaires, et le compteur garantit
que la valeur reste en vigueur tant qu'au moins un propriétaire existe encore.
Grâce aux références immuables, Rc<T>
vous permet de partager des données
entre plusieurs éléments de votre programme pour uniquement les lire. Si Rc<T>
vous avait aussi permis d'avoir des références mutables, vous auriez alors
violé une des règles d'emprunt vues au chapitre 4 : les emprunts mutables
multiples à une même donnée peuvent causer des accès concurrents et des
incohérences. Cependant, pouvoir modifier des données reste très utile ! Dans la
section suivante, nous allons voir le motif de mutabilité interne et le type
RefCell<T>
que vous pouvez utiliser conjointement avec un Rc<T>
pour pouvoir
travailler avec cette contrainte d'immuabilité.