Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an alternative or way to have Rc<RefCell<X>> that restricts mutability of X?

For example given this code:

use std::rc::Rc;
use std::cell::RefCell;

// Don't want to copy for performance reasons
struct LibraryData {
    // Fields ...
}

// Creates and mutates data field in methods
struct LibraryStruct {
    // Only LibraryStruct should have mutable access to this
    data: Rc<RefCell<LibraryData>>
}

impl LibraryStruct {
    pub fn data(&self) -> Rc<RefCell<LibraryData>> {
        self.data.clone()
    }
}

// Receives data field from LibraryStruct.data()
struct A {
    data: Rc<RefCell<LibraryData>>
}

impl A {
    pub fn do_something(&self) {
        // Do something with self.data immutably

        // I want to prevent this because it can break LibraryStruct
        // Only LibraryStruct should have mutable access 
        let data = self.data.borrow_mut();
        // Manipulate data
    }
}

How can I prevent LibraryData from being mutated outside of LibraryStruct? LibraryStruct should be the only one able to mutate data in its methods. Is this possible with Rc<RefCell<LibraryData>> or is there an alternative? Note I'm writing the "library" code so I can change it.

like image 382
Jake Avatar asked Sep 01 '18 20:09

Jake


1 Answers

If you share a RefCell then it will always be possible to mutate it - that's essentially the whole point of it. Given that you are able to change the implementation of LibraryStruct, you can make sure that data is not public, and control how it is exposed to its users through a getter method:

pub struct LibraryStruct {
    // note: not pub
    data: Rc<RefCell<LibraryData>>
}

impl LibraryStruct {
    // could also have returned `Ref<'a, LibraryData> but this hides your 
    // implementation better
    pub fn data<'a>(&'a self) -> impl Deref<Target = LibraryData> + 'a {
        self.data.borrow()
    }
}

In your other struct, you can keep things simple, by just treating it as a reference:

pub struct A<'a> {
    data: &'a LibraryData,
}

impl<'a> A<'a> {
    pub fn do_something(&self) {
        // self.data is only available immutably here because it's just a reference
    }
}

fn main() { 
    let ld = LibraryData {};
    let ls = LibraryStruct { data: Rc::new(RefCell::new(ld)) };

    let a = A { data: &ls.data() };
}

If you need to hold the reference for longer, during which time the original RefCell needs to be mutably borrowed in the library code, then you need to make a custom wrapper which can manage that. It's possible that there's a standard library type for this, but I don't know of it and it's easy to make something specifically for your use case:

// Wrapper to manage a RC<RefCell> and make it immutably borrowable
pub struct ReadOnly<T> {
    // not public
    inner: Rc<RefCell<T>>,
}

impl<T> ReadOnly<T> {
    pub fn borrow<'a>(&'a self) -> impl Deref<Target = T> + 'a {
        self.inner.borrow()
    }
}

Now return this in your library code:

impl LibraryStruct {
    pub fn data<'a>(&'a self) -> ReadOnly<LibraryData> {
        ReadOnly { inner: self.data.clone() }
    }
}

And when you use it, the inner RefCell will not be directly accessible and the data is only available to borrow immutably:

pub struct A {
    data: ReadOnly<LibraryData>,
}

impl A {
    pub fn do_something(&self) {
        //  data is immutable here
        let data = self.data.borrow();
    }
}

fn main() { 
    let ld = LibraryData {};
    let ls = LibraryStruct { data: Rc::new(RefCell::new(ld)) };

    let a = A { data: ls.data() };
}
like image 51
Peter Hall Avatar answered Oct 31 '22 04:10

Peter Hall