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

Rc<T>, le pointeur intelligent à comptage de 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 seule valeur peut avoir plusieurs propriétaires. Par exemple, dans les structures de données de graphe, plusieurs arêtes peuvent pointer vers le même noeud, et ce noeud est conceptuellement possédé par toutes les arêtes qui pointent vers lui. Un noeud ne devrait pas être nettoyé à moins qu’il n’ait plus aucune arête pointant vers lui et donc plus aucun propriétaire.

Vous devez activer la possession multiple explicitement en utilisant le type Rust Rc<T>, qui est une abréviation de reference counting (comptage de références). Le type Rc<T> suit le nombre de références vers une valeur pour déterminer si la valeur est encore utilisée ou non. S’il y a zéro références vers une valeur, celle-ci peut être nettoyée sans qu’aucune référence ne devienne invalide.

Imaginez Rc<T> comme une télévision dans un salon familial. Quand une personne entre pour regarder la télé, elle l’allume. D’autres peuvent entrer dans la pièce et regarder la télé. Quand la dernière personne quitte la pièce, elle éteint la télé car elle n’est plus utilisée. Si quelqu’un éteignait la télé alors que d’autres la regardent encore, il y aurait un tollé parmi les téléspectateurs restants !

Nous utilisons le type Rc<T> quand nous voulons allouer des données sur le tas pour que plusieurs parties de notre programme puissent les lire et que nous ne pouvons pas déterminer à la compilation quelle partie finira d’utiliser les données en dernier. Si nous savions quelle partie finirait en dernier, nous pourrions simplement faire de cette partie le propriétaire des données, et les règles normales de possession appliquées à la compilation prendraient effet.

Notez que Rc<T> est uniquement destiné aux scénarios mono-thread. Quand nous aborderons la concurrence au chapitre 16, nous couvrirons comment faire du comptage de références dans les programmes multi-threads.

Partager des données

Revenons à notre exemple de liste cons de l’encart 15-5. Rappelez-vous que nous l’avons définie avec Box<T>. Cette fois, nous allons créer deux listes qui partagent toutes les deux la possession d’une troisième liste. Conceptuellement, cela ressemble à la figure 15-3.

Une liste chaînée avec l’étiquette 'a' pointant vers trois éléments. Le premier élément contient 5 et un pointeur vers l’élément suivant, le deuxième élément contient 10 et un pointeur vers l’élément suivant, et le troisième élément contient la variante Nil, qui signale la fin de la liste. Figure 15-3 : Deux listes, `b` et `c`, partageant la possession d’une troisième liste, `a`

Nous allons créer la liste a qui contient 5 puis 10. Ensuite, nous créerons deux autres listes : b qui commence par 3 et c qui commence par 4. Les listes b et c continueront ensuite vers la première liste a contenant 5 et 10. En d’autres termes, les deux listes partageront la première liste contenant 5 et 10.

Essayer d’implémenter ce scénario en utilisant notre définition de List avec Box<T> ne fonctionnera pas, comme montré dans l’encart 15-17.

Filename: 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));
}
Listing 15-17: Demonstrating that we’re not allowed to have two lists using Box<T> that try to share ownership of a third list

Quand nous compilons ce code, nous obtenons cette erreur : console {{#include ../listings/ch15-smart-pointers/listing-15-17/output.txt}}

$ 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
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

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

Les variantes Cons possèdent les données qu’elles contiennent, donc quand nous créons la liste b, a est déplacé dans b et b possède a. Ensuite, quand nous essayons d’utiliser a à nouveau lors de la création de c, nous ne sommes pas autorisés car a a été déplacé.

Nous pourrions modifier la définition de Cons pour contenir des références à la place, mais alors nous devrions spécifier des paramètres de durée de vie. En spécifiant des paramètres de durée de vie, nous spécifierions que chaque élément de la liste vivra au moins aussi longtemps que la liste entière. C’est le cas pour les éléments et les listes de l’encart 15-17, mais pas dans tous les scénarios.

À la place, nous allons changer notre définition de List pour utiliser Rc<T> au lieu de Box<T>, comme montré dans l’encart 15-18. Chaque variante Cons contiendra maintenant une valeur et un Rc<T> pointant vers une List. Quand nous créons b, au lieu de prendre la possession de a, nous clonerons le Rc<List> que a contient, augmentant ainsi le nombre de références de un à deux et permettant à a et b de partager la possession des données dans ce Rc<List>. Nous clonerons aussi a lors de la création de c, augmentant le nombre de références de deux à trois. Chaque fois que nous appelons Rc::clone, le compteur de références vers les données dans le Rc<List> augmentera, et les données ne seront pas nettoyées à moins qu’il n’y ait zéro références 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));
}
Listing 15-18: A definition of List that uses Rc<T>

Nous devons ajouter une instruction use pour amener Rc<T> dans la portée car il n’est pas dans le prélude. Dans main, nous créons la liste contenant 5 et 10 et la stockons dans un nouveau Rc<List> dans a. Ensuite, quand nous créons b et c, nous appelons la fonction Rc::clone et passons une référence vers le Rc<List> dans a comme argument.

Nous aurions pu appeler a.clone() au lieu de Rc::clone(&a), mais la convention de Rust est d’utiliser Rc::clone dans ce cas. L’implémentation de Rc::clone ne fait pas une copie profonde de toutes les données comme le font la plupart des implémentations de clone des autres types. L’appel à Rc::clone ne fait qu’incrémenter le compteur de références, ce qui ne prend pas beaucoup de temps. Les copies profondes de données peuvent prendre beaucoup de temps. En utilisant Rc::clone pour le comptage de références, nous pouvons distinguer visuellement les clones de type copie profonde des clones qui augmentent le compteur de références. Quand on cherche des problèmes de performance dans le code, nous n’avons besoin de considérer que les clones de copie profonde et pouvons ignorer les appels à Rc::clone.

Cloner pour augmenter le compteur de références

Modifions notre exemple fonctionnel de l’encart 15-18 pour que nous puissions voir les compteurs de références changer au fur et à mesure que nous créons et libérons des références vers le Rc<List> dans a.

Dans l’encart 15-19, nous allons modifier main pour qu’il ait une portée intérieure autour de la liste c ; ensuite, nous pourrons voir comment le compteur de références change quand c sort de la portée.

Filename: src/main.rs
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

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

// --snip--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
Listing 15-19: Printing the reference count

À chaque point du programme où le compteur de références change, nous affichons le compteur de références, que nous obtenons en appelant 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 weak_count dans [“Prévenir les cycles de références avec Weak<T>”][preventing-ref-cycles].

Ce code affiche ce qui suit : console {{#include ../listings/ch15-smart-pointers/listing-15-19/output.txt}}

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Nous pouvons voir que le Rc<List> dans a à un compteur de références initial de 1 ; ensuite, chaque fois que nous appelons clone, le compteur augmente de 1. Quand c sort de la portée, le compteur diminue de 1. Nous n’avons pas besoin d’appeler une fonction pour diminuer le compteur de références comme nous devons appeler Rc::clone pour l’augmenter : l’implémentation du trait Drop diminue automatiquement le compteur de références quand une valeur Rc<T> sort de la portée.

Ce que nous ne pouvons pas voir dans cet exemple, c’est que quand b puis a sortent de la portée à la fin de main, le compteur est à 0, et le Rc<List> est complètement nettoyé. Utiliser Rc<T> permet à une seule valeur d’avoir plusieurs propriétaires, et le compteur garantit que la valeur reste valide tant qu’un des propriétaires existe encore.

Via des références immuables, Rc<T> vous permet de partager des données entre plusieurs parties de votre programme en lecture seule. Si Rc<T> vous permettait d’avoir aussi plusieurs références mutables, vous pourriez violer l’une des règles d’emprunt discutées au chapitre 4 : les emprunts mutables multiples vers le même endroit peuvent provoquer des courses de données et des incohérences. Mais pouvoir modifier des données est très utile ! Dans la prochaine section, nous aborderons le patron de mutabilité intérieure et le type RefCell<T> que vous pouvez utiliser conjointement avec un Rc<T> pour contourner cette restriction d’immuabilité.