Variables and mutability in Rust (let vs let mut)
Why Rust variables are immutable by default, when to use let mut, const vs let, type inference, and the E0384 error explained.
You create a variable with let. Rust works out the type from the value (type inference), or you can write it yourself after a colon:
fn main() {
let lives = 3;
let speed: f64 = 2.5;
println!("{lives} lives, speed {speed}");
}
3 lives, speed 2.5
Immutable by default
A variable made with let can't be changed after it gets its value. This is immutability, and it's the default on purpose: when you read code and see let total = ..., you know total keeps that value for the rest of its scope. Fewer moving parts means fewer bugs, and it's one of the things that lets the compiler check concurrent code.
Trying to assign again is a compile error:
fn main() {
let score = 10;
println!("{score}");
score = 20;
println!("{score}");
}
error[E0384]: cannot assign twice to immutable variable `score`
--> src/main.rs:4:5
|
2 | let score = 10;
| ----- first assignment to `score`
3 | println!("{score}");
4 | score = 20;
| ^^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut score = 10;
| +++
let mut
When a value really needs to change (a counter, a running total, a buffer you fill), opt in with let mut. The mut is a signal to readers: "watch this one, it changes". If you write mut and never change the variable, the compiler warns variable does not need to be mutable, so mut always means something.
const
A constant is a value fixed at compile time, written in SCREAMING_SNAKE_CASE with its type spelled out: const MAX_PLAYERS: u32 = 8;. Use const for values that are part of the program's design (limits, conversion factors), and let for values computed while the program runs.
let |
let mut |
const |
|
|---|---|---|---|
| Can change | no | yes | no |
| Type annotation | optional | optional | required |
| Value known | at run time | at run time | at compile time |
| Where | inside functions | inside functions | anywhere, including the top level |
Naming and unused variables
Variables and functions use snake_case (player_name), types use CamelCase, constants use SCREAMING_SNAKE_CASE. The compiler warns about other styles. An unused variable also gets a warning; if it's unused on purpose, start its name with an underscore (_unused).
Common mistakes
- Adding
muteverywhere "just in case". Start without it and add it when the compiler asks (itshelp:line even shows where). - Declaring a variable without a value and reading it.
let x: i32;followed byprintln!("{x}")is error E0381, "used bindingxisn't initialized". Rust never lets you read garbage. - Confusing
mutwith shadowing.let x = x + 1;makes a new variable instead of changing the old one; see the shadowing page.
Example
const MAX_LIVES: u32 = 3;
fn main() {
let player = "Ferris";
let mut lives = MAX_LIVES;
println!("{player} starts with {lives} lives");
lives -= 1;
println!("{player} lost one: {lives} left");
let mut coins = 0;
for _ in 0..4 {
coins += 25;
}
println!("coins collected: {coins}");
let _unused_bonus = 10;
}
Output:
Ferris starts with 3 lives
Ferris lost one: 2 left
coins collected: 100