Rust

Any donation is very welcome
Fork me on GitHub

III. Aller plus loin

4. 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

Les RefCell sont utiles 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 fenêtre 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) {
        for vue in &mut self.vues { // nous voulons &mut Vue et pas juste Vue
            vue.borrow_mut().pos.x += 1;
        }
        for layout in &mut self.layouts { // pareil que pour la boucle précédente
            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

Pour faire simple, le type Rc est un compteur de référence d'un objet constant (d'où son nom d'ailleurs). Exemple :

Runuse std::rc::Rc;

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

Juste 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(5);
println!("{:?} = {}", (&*r) as *const i32, *r);
let r2 = r.clone();
*Rc::make_mut(&mut r) = 10;
println!("{:?} = {}\n{:?} = {}", (&*r2) as *const i32, *r2, (&*r) as *const i32, *r);

Comme vous vous en serez rendu compte, l'objet contenu par r a changé. Pour éviter qu'une copie soit faite lorsque vous manipulez un type, il vous faudra passer par les types Cell ou RefCell. Cela pourra vous être très utile si vous avez des soucis avec des closures notamment.