Multiple calls to borrow_mut on a stack. How to do it effectively?


#1

I have got a structure like this:

struct Data {
...
}

struct SharedData{
     shared: Rc<RefCell<Data>>
}

and I have got call stack, f1 calls f2, where each wants to access shared and mutate it, like this:

impl SharedData {
    fn f1(&self) {
        let data: &mut Data = &mut self.shared.borrow_mut();
        // do something with data
        data.field = ....
        // call f2
        self.f2()
        // do something more with data
        data.field = ....
    }

    fn f2(&self) {
        let data: &mut Data = &mut self.shared.borrow_mut(); // here it is already taken in f1
        // do something with data
        data.field = ....
    }
}

To solve the problem, I am wrapping access to shared in blocks, like this:

fn f1(&self) {
    let intermediate_result = {
        let data: &mut Data = &mut self.shared.borrow_mut();
        // do something with data
        data.field = ....
    }
    // call f2
    self.f2()
    {
        let data: &mut Data = &mut self.shared.borrow_mut(); // take it again
        // do something more with data
        data.field = ....
    }
}

but find it very ugly and not convenient and dangerous as errors are spotted only at runtime.

Another alternative, I looked at, is to move f1 and f2 methods impl Data, where borrowing would not be required as self of Data would be already in the scope. However, it also does not work for me because f1 and f2 spawn async tasks and I need to clone access to Data via Rc to do following up actions on futures then callbacks.

What is the most effective way to deal with this type of a constraint in Rust?


#2

After experimenting for few hours, I have found that the safest and most elegant way to express this is to move f1 and f2 to impl Data and change their signature to have access to self (Data) and SharedData for async code. Something like this:

impl Data { // moved out of SharedData
    fn f1(&self, shared_self: SharedData) {
        let data = self;
        // do something with data
        data.field = ....
        // call f2
        data.f2(shared_self.clone())
        // do something more with data
        data.field = ....
        // and still shared_self can be used in async code, like:
        // let data: &mut Data = &mut shared_self.shared.borrow_mut();
    }

    fn f2(&self, shared_self: SharedData) {
        let data = self; // works and not collision on nested borrowing
        // do something with data
        data.field = ....
        // and still shared_self can be used in async code, like:
        // let data: &mut Data = &mut shared_self.shared.borrow_mut();
    }
}

I hope it helps somebody. It could be useful to capture this pattern to a collection of Effective Rust, if one exists ?


#3

I tend to prefer your first solution. I find it simpler to understand.


#4

Yes, it is more natural. But more dangerous as errors are spotted only in runtime


#5

The last solution caught majority of borrow conflicts statically


#6

Approaches to things like this are discussed in this other thread. I feel like I’ve been linking to it a bit much lately but I’d have nothing new to say here :slight_smile:.

Using explicit field borrows and associated functions (rather than methods) will make things easier.


#7

Here is what end up with:

use std::collections::HashMap;

fn main() {
    let mut d = Data::default();
    d.mutate();
}

#[derive(Default)]
struct Cache {
    field: i32
}

#[derive(Default)]
struct Data {
    map: HashMap<i32, Cache>,
    field2: String,
}

impl Data {
    fn mutate(&mut self) -> () {
        let c = self.map.entry(0).or_insert_with(|| Cache::default());
        //if self.field2.is_empty() { // <-- this works
        if Self::read_only_helper(&self.field2) { // <-- this also works
            c.field = 1;
        }
    }
    fn read_only_helper(d: &String)->bool {
        // returns something dummy for example
        d.is_empty()
    }
}

Hope it is helpful for somebody.