Thanks!
X
Paypal
Github sponsorship
Patreon
Acheter version papier

Rust

Fork me on GitHub

III. Aller plus loin

5. Rc et RefCell

Ce chapitre va vous permettre de comprendre encore un peu plus le fonctionnement du borrow-checker de Rust au travers des types RefCell et Rc.

RefCell

Le type RefCell est utile pour garder un accès mutable sur un objet. Le "borrowing" est alors vérifié au runtime plutôt qu'à la compilation.

Imaginons que vous vouliez dessiner une interface graphique contenant plusieurs vues. Ces vues seront mises dans un layout pour faciliter leur agencement dans la fenêtre. Seulement, on ne peut pas s'amuser à créer un vecteur contenant une liste de références mutables sur un objet, ça ne serait pas pratique du tout !

Runstruct Position {
    x: i32,
    y: i32,
}

impl Position {
    pub fn new() -> Position {
        Position {
            x: 0,
            y: 0,
        }
    }
}

struct Vue {
    pos: Position,
    // plein d'autres champs
}

struct Layout {
    vues: Vec<&mut Vue>,
    layouts: Vec<&mut Layout>,
    pos: Position,
}

impl Layout {
    pub fn update(&mut self) {
        for vue in self.vues {
            vue.pos.x += 1;
        }
        for layout in self.layouts {
            layout.update();
        }
    }
}

fn main() {
    let mut vue1 = Vue { pos: Position::new() };
    let mut vue2 = Vue { pos: Position::new() };
    let mut lay1 = Layout {
        vues: vec!(), layouts: vec!(), pos: Position::new(),
    };
    let mut lay2 = Layout {
        vues: vec!(), layouts: vec!(), pos: Position::new(),
    };

    lay1.vues.push(&mut vue1);
    lay2.layouts.push(&mut lay1);
    lay2.vues.push(&mut vue2);
    lay2.update();
}

Si on compile le code précédent, on obtient :

<anon>:23:15: 23:23 error: missing lifetime specifier [E0106]
<anon>:23     vues: Vec<&mut Vue>,
                        ^~~~~~~~
<anon>:23:15: 23:23 help: see the detailed explanation for E0106
<anon>:24:18: 24:29 error: missing lifetime specifier [E0106]
<anon>:24     layouts: Vec<&mut Layout>,
                           ^~~~~~~~~~~
<anon>:24:18: 24:29 help: see the detailed explanation for E0106
error: aborting due to 2 previous errors

"Arg ! Des lifetimes !"

En effet. Et réussir à faire tourner ce code sans soucis va vite devenir très problématique ! C'est donc là qu'intervient RefCell. Il permet de "balader" une référence mutable et de ne la récupérer que lorsque l'on en a besoin avec les méthodes borrow et borrow_mut. Exemple :

Runuse std::cell::RefCell;

struct Position {
    x: i32,
    y: i32,
}

impl Position {
    pub fn new() -> Position {
        Position {
            x: 0,
            y: 0,
        }
    }
}

struct Vue {
    pos: Position,
    // plein d'autres champs
}

struct Layout {
    vues: Vec<RefCell<Vue>>,
    layouts: Vec<RefCell<Layout>>,
    pos: Position,
}

impl Layout {
    pub fn update(&mut self) {
        // Nous voulons "&mut Vue" et pas juste "Vue".
        for vue in &mut self.vues {
            vue.borrow_mut().pos.x += 1;
        }
        // Pareil que pour la boucle précédente.
        for layout in &mut self.layouts {
            layout.borrow_mut().update();
        }
    }
}

fn main() {
    let mut vue1 = Vue { pos: Position::new() };
    let mut vue2 = Vue { pos: Position::new() };
    let mut lay1 = Layout {
        vues: vec!(), layouts: vec!(), pos: Position::new(),
    };
    let mut lay2 = Layout {
        vues: vec!(), layouts: vec!(), pos: Position::new(),
    };

    lay1.vues.push(RefCell::new(vue1));
    lay2.layouts.push(RefCell::new(lay1));
    lay2.vues.push(RefCell::new(vue2));
    lay2.update();
}

Rc

Le type Rc est un compteur de référence (d'où son nom d'ailleurs, "reference counter"). Exemple :

Runuse std::rc::Rc;

let r = Rc::new(5);
println!("{}", *r);

Jusque là, rien de problématique. Maintenant, que se passe-t-il si on clone ce Rc ?

Runuse std::rc::Rc;

let r = Rc::new(5);
let r2 = r.clone();
println!("{}", *r2);

Rien de particulier, r et r2 pointent vers la même valeur. Et si on modifie la valeur de l'un des deux ?

Runlet mut r = Rc::new("a".to_owned());
println!("1. {:?} = {}", (&*r) as *const String, *r);
let r2 = r.clone();
*Rc::make_mut(&mut r) = "b".to_owned();
println!("2. {:?} = {}", (&*r2) as *const String, *r2);
println!("3. {:?} = {}", (&*r) as *const String, *r);

Ce code affichera :

1. 0x55769a45c920 = a
2. 0x55769a45c920 = a
3. 0x55769a45ca20 = b

Les valeurs de r et de r2 ne sont plus les mêmes et leur pointeur non plus. La raison est la suivante : make_mut va vérifier si il y a une autre copie de ce pointeur. Si c'est le cas, pour éviter de faire une opération unsafe qui serait de modifier de la mémoire partagée, il va cloner le contenu et créer un nouveau pointeur vers ce contenu dupliqué pour pouvoir le modifier.

Pour éviter qu'une copie ne soit faite lorsque vous manipulez Rc, il vous faudra passer par les types Cell ou RefCell car ils n'ont pas besoin d'être mutable pour pouvoir modifier leur contenu comme expliqué dans ce chapitre. Cela pourra vous être très utile si vous avez des soucis avec des closures notamment.