Why does RefCell not have the same scoping as regular references?
Asked Answered
C

1

6

I can write the following function f and the code compiles as I would expect:

use std::collections::HashMap;

struct A {
    data: HashMap<u32, B>,
}

struct B {
    val: Option<u32>,
}

impl A {
    fn f(&mut self, key: u32) {
        let data = &self.data[&key];
        match data.val {
            Some(value) => panic!("{}", value),
            None => self.data.remove(&key),
        };
    }
}

Here key is checked out first immutably &self.data[&key], and then again mutably (self.data.remove(&key)), but the compiler allows this, likely because data is never used after the mutable checkout.

However, if I then use a RefCell rather than just a regular ref, I get a compile time error, even though the logic is otherwise (seemingly) the same:

use std::collections::HashMap;
use std::cell::RefCell;

struct A {
    data: HashMap<u32, RefCell<B>>,
}

struct B {
    val: Option<u32>,
}

impl A {
    fn f(&mut self, key: u32) {
        let data = self.data[&key].borrow();
        match data.val {
            Some(value) => panic!("{}", value),
            None => self.data.remove(&key),
        };
    }
}

The error is:

error[E0502]: cannot borrow `self.data` as mutable because it is also borrowed as immutable
  --> src/main.rs:17:21
   |
14 |         let data = self.data[&key].borrow();
   |                    --------- immutable borrow occurs here
...
17 |             None => self.data.remove(&key),
   |                     ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
18 |         };
19 |     }
   |     - immutable borrow might be used here, when `data` is dropped and runs the destructor for type `Ref<'_, B>`

My guess is that the compiler isn't able to determine that I finished with data, due to it being a RefCell and thus checked at runtime, but if that's the case, is there any way I can 'check in' the value that I borrowed, since its no longer needed? Or is my only option to let it go out of scope? (Which is fine, for this small example, but aesthetically displeasing with larger ones.)

Caia answered 6/10, 2023 at 0:46 Comment(0)
A
10

My guess is that the compiler isn't able to determine that I finished with data

The compiler could, but it chooses not to, in order to avoid worse surprises. The specific rule here is that

  • values of types which have Drop code (such as std::cell::Ref) always have that code run at the end of scope,
  • which means that any borrows held by those values also extend to end of scope.

Drop timing/ordering matters because it can have side effects, so the compiler keeps it predictable.

is there any way I can 'check in' the value that I borrowed, since its no longer needed?

Explicitly drop it. Change your match like this and it will compile:

None => {
    drop(data);
    self.data.remove(&key);
}

Now the compiler sees that data no longer exists and is no longer borrowing self.data, so it lets you borrow self.data exclusively afterward.

Axon answered 6/10, 2023 at 2:30 Comment(2)
Neat. I suppose in that case, you could also force the drop by fetching val in an isolated scope? Something like match { let data = self.data[&key].borrow(); data.val } { ... } ?Martine
@Martine Yes, but the cases where that works are also often cases where you can use Cell instead of RefCell anyway (since you're not taking a reference to the content.Axon

© 2022 - 2025 — McMap. All rights reserved.