const, static and interior mutability
Rust is immutable by default in three places: bindings, items and fields behind a shared reference. The first two are settled by mut, and the third is the one that needs a concept. A method that takes &self cannot write to a field, and if it must count or cache, the field has to be a cell that allows controlled mutation.
Coming from C++? const and mut shows a C++ getter with a side effect, which compiles and runs, next to the Rust method that the compiler stops with E0594.
This lesson covers the two item kinds, const and static, then the two standard cells, Cell and RefCell. The cells are the only safe way to mutate through a shared reference, and they are the foundation of the smart pointers in the next part of the course.
const items
A const item is computed at compile time and substituted wherever it is used. It has no address and no storage of its own, and its type is mandatory. It may be used in another constant, in an array length and in a pattern.
The program divides one constant by four at compile time and prints both values. The division happens before the program runs, so there is no instruction for it in the binary.
const MAX_ITEMS: usize = 100;
fn main() {
println!("{MAX_ITEMS}");
let limit = MAX_ITEMS / 4;
println!("{limit}");
}
rustc --edition 2024 const_items.rs
./const_items
100
25
A constant that cannot be evaluated at compile time is an error, not a lazy initialiser. The lesson on const fn shows how far the evaluator goes.
static values
A static item has one address for the whole program and is not inlined. It is the right tool for a value that must exist once, such as a global string or a lookup table. A mutable static exists but is unsafe to touch, because two threads could write it at once.
The program reads two immutable statics and prints them. Nothing is allocated and nothing is copied: the values live in the binary and the program borrows them.
static GREETING: &str = "hello";
static COUNT: usize = 7;
fn main() {
println!("{GREETING} {COUNT}");
}
rustc --edition 2024 static_item.rs
./static_item
hello 7
For a value that is computed once at run time, the course uses a cell or a thread-safe initialiser later; a plain static is for constants that never change. The lesson on Send and Sync explains why a mutable one needs unsafe.
Cell: a counter behind &self
Cell<T> allows a value to be replaced through a shared reference. It has no borrow checking at run time: get copies the value out and set writes a new one, which is why it works for Copy types such as integers.
The counter below has a Cell<u32> field and a bump method on &self. The method writes, the caller keeps an immutable binding, and the output is two.
use std::cell::Cell;
struct Counter {
value: Cell<u32>,
}
impl Counter {
fn bump(&self) {
self.value.set(self.value.get() + 1);
}
}
fn main() {
let counter = Counter { value: Cell::new(0) };
counter.bump();
counter.bump();
println!("{}", counter.value.get());
}
rustc --edition 2024 cell.rs
./cell
2
The cell is not a way around the borrow checker: it is a type whose interior mutation is allowed by design, and the compiler still knows that no reference into it escapes. The lesson on Rc and RefCell combines it with shared ownership.
RefCell: a checked borrow
RefCell<T> allows mutation through a shared reference for any type, not only Copy ones, and it checks the borrow rules at run time. borrow() gives a shared reference, borrow_mut() a mutable one, and a second conflicting borrow panics instead of failing the build.
The program adds five to the value through a mutable borrow and prints it through a shared one. The two borrows are sequential, so the run-time check passes and the output is fifteen.
use std::cell::RefCell;
fn main() {
let value = RefCell::new(10);
*value.borrow_mut() += 5;
println!("{}", value.borrow());
}
rustc --edition 2024 refcell.rs
./refcell
15
The panic is the price of moving a compile-time check to run time. The lesson on RefCell shows the double borrow that panics and the message it prints.
Writing behind a shared reference
Without a cell, a method on &self cannot assign to a field. The compiler reports E0594, and the help offers &mut self as the alternative. That is the error this lesson exists to explain: the fix is either a mutable receiver or a cell, and the difference is visible in the API.
The program writes through &self and prints the field. The error points at the assignment, not at the call, because the method itself is well formed apart from the write.
struct Counter {
value: u32,
}
impl Counter {
fn bump(&self) {
self.value += 1;
}
}
fn main() {
let counter = Counter { value: 0 };
counter.bump();
println!("{}", counter.value);
}
rustc --edition 2024 behind_ref_err.rs
error[E0594]: cannot assign to `self.value`, which is behind a `&` reference
--> behind_ref_err.rs:7:9
|
7 | self.value += 1;
| ^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be written to
|
help: consider changing this to be a mutable reference
|
6 | fn bump(&mut self) {
| +++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0594`.
Choosing between the two fixes is an API decision: &mut self tells the caller that the value changes, while a cell keeps the method callable on a shared reference and moves the conflict to run time.
Exercises
- Use the constant in an array length and print the array. Which value has to be a usize?
- Move the counter field into a Cell and make bump take &self. Does the caller need mut now?
- Borrow a RefCell twice at once in a small program. What does the panic say, and which line is named?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the difference between const and static?
Answer
A const is inlined and has no address; a static has one address
A constant is substituted where it is used, while a static is one place in memory.
-
What does Cell allow that a plain field does not?
Answer
Replacing the value through a shared reference
Cell provides interior mutability for Copy values without a run-time borrow check.
-
What does RefCell check?
Answer
The borrow rules at run time
RefCell allows the borrows and panics when two of them conflict, instead of failing the build.
-
What is the fix for E0594 in a getter that writes?
Answer
Take &mut self, or put the field in a cell
Either the receiver is allowed to write, or the field provides interior mutability.
-
What does this program print?
const MAX_ITEMS: usize = 100; fn main() { println!("{MAX_ITEMS}"); let limit = MAX_ITEMS / 4; println!("{limit}"); }Answer
100 / 25
The constant is a hundred, and a quarter of it is twenty-five.
-
What does this program print?
static GREETING: &str = "hello"; static COUNT: usize = 7; fn main() { println!("{GREETING} {COUNT}"); }Answer
hello 7
Both statics hold their values for the whole program, and the print reads them.
-
What does this program print?
use std::cell::Cell; struct Counter { value: Cell<u32>, } impl Counter { fn bump(&self) { self.value.set(self.value.get() + 1); } } fn main() { let counter = Counter { value: Cell::new(0) }; counter.bump(); counter.bump(); println!("{}", counter.value.get()); }Answer
2
The cell is written through a shared reference, and two bumps leave two.
-
What does this program print?
use std::cell::RefCell; fn main() { let value = RefCell::new(10); *value.borrow_mut() += 5; println!("{}", value.borrow()); }Answer
15
The mutable borrow adds five, and the shared borrow prints the new value.
-
What is the first line of the compiler output for this program?
struct Counter { value: u32, } impl Counter { fn bump(&self) { self.value += 1; } } fn main() { let counter = Counter { value: 0 }; counter.bump(); println!("{}", counter.value); }Answer
error[E0594]: cannot assign to
self.value, which is behind a&referenceThe method has a shared receiver, so the field assignment is rejected inside the method.
-
What does this program print?
const SCALE: i32 = 3; fn main() { let value = SCALE * 7; println!("{value}"); }Answer
21
The constant is three and the product is twenty-one, computed at compile time.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev