RustArena

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

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

Practice it