Is RefCell ever useful on its own?

Is there a significant difference between owning a value, and having a single RefCell that contains it? It seems to me that the only situation where you'd use RefCell is in conjunction with Rc, so that there is some use to the dynamic borrow-checking.

I'm afraid I'm missing something and that my understanding here might be incomplete.

Rc is for shared ownership, but you could also just be sharing plain & references to your type in multiple places, and you may need to be able to mutate it.

1 Like

I think I understand Rc. Using RefCell alone is what's confusing me.

Imagine that a dependency, which you aren't able to change, defines the following trait:

trait WidgetCalculator {
    fn get_total_widgets(&self) -> i32;
}

In my crate, I've implemented this trait:

struct SlowWidgetCalculator {
    db: WidgetDatabase,
}

impl WidgetCalculator for SlowWidgetCalculator {
    fn get_total_widgets(&self) -> i32 {
        self.db.extremely_slow_database_query()
    }
}

As the name would suggest, the database query is extremely slow, so I'd like to cache the result:

struct SlowWidgetCalculator {
    db: WidgetDatabase,
    cache: Option<i32>,
}

impl WidgetCalculator for SlowWidgetCalculator {
    fn get_total_widgets(&self) -> i32 {
        match self.cache {
            Some(value) => value,
            None => {
                let value = self.db.extremely_slow_database_query();
                self.cache = Some(value);
                value
            }
        }
    }
}

But wait, now the compiler is angry at me:

error[E0594]: cannot assign to `self.cache` which is behind a `&` reference
  --> src/lib.rs:22:17
   |
17 |     fn get_total_widgets(&self) -> i32 {
   |                          ----- help: consider changing this to be a mutable reference: `&mut self`
...
22 |                 self.cache = Some(value);
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written

I can't change the definition of get_total_widgets, because it's not in my crate, and I can't avoid mutating self if I want to store the data :frowning:

Here comes RefCell to save the day:

struct SlowWidgetCalculator {
    db: WidgetDatabase,
    cache: RefCell<Option<i32>>,
}

impl WidgetCalculator for SlowWidgetCalculator {
    fn get_total_widgets(&self) -> i32 {
        let mut cache = self.cache.borrow_mut();
    
        match *cache {
            Some(value) => value,
            None => {
                let value = self.db.extremely_slow_database_query();
                *cache = Some(value);
                value
            }
        }
    }
}

This is all a very long winded way of saying: RefCell is useful whenever you need to modify some data, but all you have is a & reference. A lot of the time, you can get around the need to do this by restructuring your program, but in some cases (like when ownership of the data is shared via Rc, or when you don't have control over the type of reference you get), you need the flexibility of a runtime check.

Runnable version of the final example: Rust Playground

17 Likes

Tysm, makes perfect sense!

1 Like

Of course in this case, a Cell would suffice as Option<i32> is copy.

1 Like

The RefCell type is actually a hack regarding the limits of Rust's affine (also called linear) type system: Its default take is that a value should be multiply immutably borrowable*, or it should be singly mutably borrowable, but not both at the same time.

Where RefCell comes in is in e.g. graphs, where there isn't 1 single owner of the nodes. One way to deal with that is indeed using Rc<RefCell>. This combo solves the issue by having Rc provide shared ownership. The thing is, a value in an Rc is immutable when there are multiple owners, because attempting to get a &mut to the value inside will fail at runtime.

This is the problem RefCell solves: it allows you to use a shared borrow to mutate the value inside, and the way it stays sound is by pushing the borrow checks to runtime. This is why I call it a hack: it's a necessary solution to an extant problem (one that I've used myself in the past), but as far as solutions go, it's not a nice one.

*technically they are shared borrows, which don't necessarily enforce immutability

1 Like

If you're going to use an Arc, you'll need a Mutex instead of a RefCell.

If you mean because of atomicity (i.e. the same reason to use Arc in the first place) then fair enough, alternatively an RwLock can also be used, which can be advantageous when there are multiple readers since it works a lot like a borrow: either any number of readers allowed at a time, or a single writer.
In contrast, a Mutex only allows one access (read or write) at a time. (source)

I'll fix the post.

EDIT: apparently there's no strikethrough option in the rich text editor of this forum, so I removed the references to Arc.

Strikethrough: You can always just use HTML encoding – surround the text where you want the strikethrough with <s> and </s>. I've used that in a number of posts on this forum.

2 Likes

~~strikethrough~~ gives strikethrough


I wouldn't call shared mutability a hack, since that would mean calling all other languages a hack. Indeed, the only language having &mut references is Rust : whereas all the other languages mainly think in terms of mutability and only deal with exclusive access when explicitely doing parallel programming, Rust makes exclusive access a first-class paradigm, baked into the language.

But not all patterns can guarantee compile-time exclusive access, so sometimes Rust needs to go back down to classic programming paradigms, such as Cell and RwLock.

True, of all the shared mutability types, RefCell is the ugly one, as its usage can very often be replaced through a well designed non-unsafe API (e.g., one using Cells when single-threaded, and a synchronization primitive when multi-threaded), or go down to the level of the other languages and offer unchecked unsafe APIs (UnsafeCell based).

For instance, while it is true that C++'s int32_t const & type can be translated to Rust as a &'_ i32,

it is wrong to translate C++'s int32_t & type to &'_ mut i32: no other language has &mut references!

  • The proper translation would be &'_ Cell<i32> (and more generally, T & becomes &'_ UnsafeCell<T>).

Regarding the topic at hand, Rc is not the only shared pointer type ; RefCell (and other shared mutability wrappers) can be used with any shared pointer type, such as & _: it all depends on whether you want the lifetimes of the references be checked at compile-time or be controlled at runtime. That is, you can build a doubly linked list abstraction in Rust using Cell<Option<&'stack_frame Node>> as its pointer type, but by not using Rc the list will not be able to outlive the stack frame it was created in.

3 Likes

Well I didn't call shared mutability itself a hack, I called the Rust implementation of it a hack.

That said, the concept of shared mutability is not sound in general (hence Rust's ownership system) so it can definitely be regarded as a hack. That many languages implement that hack, doesn't make it any less so, same as when most people believed the earth was flat. That didn't make the earth any flatter either.

Indeed, hence my observation that it is a hack intended to work around the limitations of the type system rules.

It's a hack because it doesn't fit neatly into the proper architecture, and instead requires the programmer to do things that are considered unnatural or unidiomatic under pretty much any other circumstance.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.