rust Internal Variability

This article refers to rust book ch15 and adds its own understanding, interested parties can first look at the official documentation.

Rust has two ways to achieve mutability

  • Inheritance variability: for example, if a struct is declared with let mut, then any field of the struct can be modified later.
  • Internal mutability: use Cell RefCell to wrap a variable or field so that it can be modified even if the external variable is read-only

It seems that inheritance mutability is enough, so why do we need the so-called interior mutability internal mutability? Let’s analyze two examples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct Cache {
    x: i32,
    y: i32,
    sum: Option<i32>,
}

impl Cache {
    fn sum(&mut self) -> i32 {
        match self.sum {
            None => {self.sum=Some(self.x + self.y); self.sum.unwrap()},
            Some(sum) => sum,
        }
    }
}

fn main() {
    let i = Cache{x:10, y:11, sum: None};
    println!("sum is {}", i.sum());
}

The structure Cache has three fields, x , y , sum , where sum simulates the lazy init lazy loading mode, the above code is not working, the reason is simple, when let initialize the variable i, is immutable.

1
2
3
4
17 |     let i = Cache{x:10, y:11, sum: None};
   |         - help: consider changing this to be mutable: `mut i`
18 |     println!("sum is {}", i.sum());
   |                           ^ cannot borrow as mutable

There are two ways to fix this problem, the let declaration specifies let mut i , but for larger projects the outer variables are likely to be immutable and immutable. This is where internal mutability comes in handy.

Fix

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::cell::Cell;

struct Cache {
    x: i32,
    y: i32,
    sum: Cell<Option<i32>>,
}

impl Cache {
    fn sum(&self) -> i32 {
        match self.sum.get() {
            None => {self.sum.set(Some(self.x + self.y)); self.sum.get().unwrap()},
            Some(sum) => sum,
        }
    }
}

fn main() {
    let i = Cache{x:10, y:11, sum: Cell::new(None)};
    println!("sum is {}", i.sum());
}

This is the code after the fix, sum type is Cell<Option<i32>> , beginners are easily confused, what the hell is this ah? Some also nest multiple wrappers, like Rc<Cell<Option<i32>> and so on.

For example, Rc represents shared ownership, but because the T in Rc<T> is read-only and cannot be modified, we need to use Cell to seal a layer so that ownership is shared, but still mutable, and Option is the common value Some(T) or the null value None , which is still very understandable.

If you are not writing rust code and just want to read the source code to understand the process, there is no need to delve into these wrappers, focus on the real type of the wrapper can be.

The example given by the official website is Mock Objects, the code is longer, but the principle is the same.

1
2
3
struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
}

Finally, it’s all about wrapping the structure fields with RefCell.

Cell

1
2
3
4
5
6
7
8
use std::cell::Cell;

fn main(){
    let a = Cell::new(1);
    let b = &a;
    a.set(1234);
    println!("b is {}", b.get());
}

This code is very representative, if the variable a is not wrapped with Cell, then it is not allowed to modify a while b read-only borrow exists, as guaranteed by the rust compiler during the compile period: Given a pair, only N immutable borrowings or one mutable borrowing are allowed to exist in scope (NLL).

Cell gets and modifies values via get / set, this function requires that value must implement the Copy trait, if we replace it with another structure, the compile will report an error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
error[E0599]: the method `get` exists for reference `&Cell<Test>`, but its trait bounds were not satisfied
  --> src/main.rs:11:27
   |
3  | struct Test {
   | ----------- doesn't satisfy `Test: Copy`
...
11 |     println!("b is {}", b.get().a);
   |                           ^^^
   |
   = note: the following trait bounds were not satisfied:
           `Test: Copy`

From the above, we can see that struct Test does not implement Copy by default, so get is not allowed. Is there any way to get the underlying struct? You can use get_mut to return a reference to the underlying data, but this requires the entire variable to be a let mut, so it is not consistent with the original intent of using Cell.

RefCell

Unlike Cell, we use RefCell to get either immutable borrowing via borrow or mutable borrowing of the underlying data via borrow_mut.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::cell::{RefCell};

fn main() {
    let cell = RefCell::new(1);

    let mut cell_ref_1 = cell.borrow_mut(); // Mutably borrow the underlying data
    *cell_ref_1 += 1;
    println!("RefCell value: {:?}", cell_ref_1);

    let mut cell_ref_2 = cell.borrow_mut(); // Mutably borrow the data again (cell_ref_1 is still in scope though...)
    *cell_ref_2 += 1;
    println!("RefCell value: {:?}", cell_ref_2);
}

Code from badboi.dev, compiled successfully, but failed to run.

1
2
3
4
5
6
7
8
9
# cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
#
# cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/hello_cargo`
RefCell value: 2
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:10:31
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

cell_ref_1 calls borrow_mut to get a variable borrow, and while it is still in scope, cell_ref_2 also tries to get a variable borrow, at which point the runtime check reports an error and panic directly.

That means RefCell moves the borrow rule from compile-time compile to runtime , with some runtime overhead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

This is the official example, used in combination with Rc , RefCell to share ownership while modifying List node values.

Summary

Internal mutability provides great flexibility, but filtered to runtime overhead, still can not be abused, performance problems are not significant, the focus is the lack of compile-time static checks, will cover up many errors.