How can I take and replace the value of a RefCell?

I have a RefCell<T>, and I want to consume the value T and replace with a new T, how can I achieve this goal?

I think the signature of the corresponding method is:

fn take_and_replace<F>(&self, f: F) 
    where F: FnOnce(T) -> T;

How about: replace_with?

The signature of replace_with is:

pub fn replace_with<F>(&self, f: F) -> T where
    F: FnOnce(&mut T) -> T

which doesn't allow us to consume the T

Does your T implement Default?

1 Like

No, T doesn't implement Default

Because panics can be caught in Rust, you need to leave some valid T inside the cell while f runs. Otherwise, accessing the cell after a panic would be UB.

As you don't have a Default value to use, you'll need to build your own temporary value to use instead.

1 Like

Then you can't do it.

I think the issue with your signature (and your problem as currently stated) is that to call f you'd first have to move the value out of the ref cell, leaving the ref cell in a temporarily deinitialized state that could be observed by shared borrows. So you either need to fabricate a temporary replacement (e.g. T::default()), T needs to be copyable, or something like that.

It might help to take a step back and explain what larger problem you're trying to solve.

2 Likes

Are there any methods to make the move and call atomic, so no other shared borrows can observe this temporarily deinitialized state?

If I only use RefCell<T> in a single thread, do I need to care about the temporarily deinitialized state?

I'm still a beginner, but with the current interface to RefCell, I don't think you can implement that because all of those operations are "single-thread atomic" and don't know what happens between operations. I think you could implement it as an unsafe primitive operation in your own RefCell-like data structure, but you'd still be left with the unwind safety issue since the call to f could panic.

1 Like

Yes; there's (at least) two scenarios that the compiler is concerned about:

  1. You might panic! between taking the old value out and putting the new one in, which will leave the cell in an undefined state when the panic is caught.
  2. The body of f() might try to borrow the cell recursively, and see it in a deinitialized state.

If you want to consume the value that's in the cell, you'll need to use something like RefCell::take(), RefCell::swap(), RefCell::replace(), or RefCell::into_inner().

One thing you can do is replace RefCell<T> with RefCell<Option<T>>, and then use None as your temporary value.

2 Likes

You can also look for something like replace_with, which will abort the program if it attempts to do something illegal.

2 Likes

There's also the domain-specific question of what happens if you observe the temporary value, even if it's well defined Rust. A "temporary None" might be semantic nonsense to the rest of the program, so they'd have no reasonable choice but to unwrap() and panic on None. So now you're adding more data (Some vs None) and runtime checks and potential panics to a type that is already weighed down by that.