Variables, Types and Functions · about 35 min

Variables, Mutability and Shadowing

Why this matters

Programs work by computing values and keeping them around under names. In most languages a variable is simply a box you can overwrite at will. Rust makes a different default choice: names are immutable unless you say otherwise. That one decision removes a whole class of "who changed this?" bugs and makes code easier to reason about, and it is your first taste of how Rust uses the compiler to enforce intentions. This lesson covers let, mut, shadowing and constants, and what they look like in memory.

let: binding a name to a value

fn main() {
    let apples = 5;
    let price = 0.25;
    let total = apples as f64 * price;
    println!("{apples} apples cost {total}");
}
5 apples cost 1.25

let creates a variable: it reserves space for a value, puts the value there, and binds a name to it. Rust programmers often say "binding" rather than "variable", because, as you will see, the name and the value are less tightly coupled than in some languages.

Every variable has a type, fixed at compile time. You did not write any types above; the compiler inferred them: 5 is an integer, so apples gets the default integer type i32 (a 32-bit signed integer); 0.25 is a float, f64. You can write the type yourself with a type annotation:

fn main() {
    let apples: i32 = 5;
    let price: f64 = 0.25;
    let initial: char = 'R';
    println!("{apples} {price} {initial}");
}
5 0.25 R

Inference never guesses at run time: by the time the program runs, every variable has one concrete type. (apples as f64 converts the integer to a float, because Rust never mixes number types silently. The next lessons cover number types and conversions in detail.)

Where variables live

Local variables live in the function's stack frame, a block of memory reserved when the function starts and released when it returns:

main's stack frame
+-----------+-------------------------+
| apples    | 5        (4 bytes, i32) |
| price     | 0.25     (8 bytes, f64) |
| total     | 1.25     (8 bytes, f64) |
+-----------+-------------------------+

The names exist only for you and the compiler; in the machine code, apples is just "the four bytes at a certain offset in the frame". Knowing a value's type tells the compiler how many bytes to reserve. Module 4 returns to the stack in detail, together with the heap.

Immutable by default

Try to change a variable:

fn main() {
    let score = 10;
    println!("score: {score}");
    score = 15;
    println!("score: {score}");
}
error[E0384]: cannot assign twice to immutable variable `score`
 --> main.rs:4:5
  |
2 |     let score = 10;
  |         ----- first assignment to `score`
3 |     println!("score: {score}");
4 |     score = 15;
  |     ^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut score = 10;
  |         +++

E0384: "cannot assign twice to immutable variable". The labels walk you through it: the first assignment happened at the let, the second is not allowed, and the help offers the fix.

Why make immutability the default? When you read let limit = 100; in a long function, you know limit is 100 everywhere below, without reading every line in between. Changing state is where bugs live, so Rust asks you to mark it.

mut: opting in to change

fn main() {
    let mut score = 10;
    println!("score: {score}");
    score = 15;
    score = score + 1;
    score += 1;
    println!("score: {score}");
}
score: 10
score: 17

mut makes the binding mutable: you may assign new values of the same type. score += 1 is shorthand for score = score + 1; there are also -=, *=, /= and %=. There is no ++ in Rust.

mut changes the value, never the type. score = "high"; would be E0308 mismatched types: the slot in the stack frame is four bytes reserved for an i32, and a piece of text does not go there.

Shadowing: a new variable with an old name

You can declare a new variable with the same name as an existing one:

fn main() {
    let input = "42";
    let input: i32 = input.parse().unwrap();
    let input = input * 2;
    println!("{input}");
}
84

Each let input creates a new variable that shadows (hides) the previous one from that point on. Nothing is modified: the old value is still there, just no longer reachable by that name. Here the first input is text, the second is a number parsed from it, and the third is twice that. (.parse().unwrap() turns text into a number; it is explained in the lesson on reading input.)

Shadowing versus mut:

mutshadowing
changes the existing valueyesno, creates a new variable
can change the typenoyes
result is immutable afterwardsnoyes, unless the new let says mut

Shadowing is idiomatic for transforming a value through steps where the intermediate forms are not interesting by themselves: raw text, then trimmed text, then a parsed number. It avoids inventing names like input_str, input_trimmed, input_num.

Shadowing respects blocks. A { ... } block opens a new scope; a let inside it lasts only until its closing brace:

fn main() {
    let x = 1;
    {
        let x = x + 10;
        println!("inner x = {x}");
    }
    println!("outer x = {x}");
}
inner x = 11
outer x = 1

Declaring now, initializing later

A variable can be declared without a value, as long as it is assigned before use:

fn main() {
    let message;
    message = "assigned once";
    println!("{message}");
}
assigned once

This is still an immutable variable: it is assigned exactly once. The compiler tracks this on every path through the code. Using it before assignment is an error:

fn main() {
    let total: i32;
    println!("{total}");
}
error[E0381]: used binding `total` isn't initialized
 --> main.rs:3:15
  |
2 |     let total: i32;
  |         ----- binding declared here but left uninitialized
3 |     println!("{total}");
  |               ^^^^^^^ `total` used here but it isn't initialized
  |
help: consider assigning a value
  |
2 |     let total: i32 = 42;
  |                    ++++

E0381. In C, reading an uninitialized variable silently gives you whatever bytes happened to be in memory. Rust refuses to compile it.

Constants

const MAX_PLAYERS: u32 = 4;
const SECONDS_PER_HOUR: u32 = 60 * 60;

fn main() {
    println!("{MAX_PLAYERS} players, {SECONDS_PER_HOUR} s per hour");
}
4 players, 3600 s per hour

A const differs from an immutable let:

Use constants for fixed facts of your program: limits, conversion factors, magic numbers that deserve a name.

Unused and underscore names

If you create a variable and never read it, you get a warning, because that often signals a bug (you computed something and then used the wrong name). A name starting with _ means "unused on purpose" and silences the warning. A lone _ is a special pattern that does not bind at all.

Common mistakes

Assigning to an immutable variable gives E0384, as shown. Add mut if the value really needs to change, or use shadowing if you are transforming it into something new.

Changing the type through mut:

fn main() {
    let mut count = 0;
    count = "zero";
}
error[E0308]: mismatched types
 --> main.rs:3:13
  |
2 |     let mut count = 0;
  |                     - expected due to this value
3 |     count = "zero";
  |             ^^^^^^ expected integer, found `&str`

Expecting shadowing to modify the outer variable. A let inside a block does not change the variable outside it, as the block example showed. If you wanted the outer value updated, you wanted mut and plain assignment (x = x + 10;), not a new let.

Using a variable before giving it a value: E0381.

Recap

Going further: try let x = 5; let x = x; and let mut x = 5; let x = x; and think about what the second line does to mutability. The Rust Book, chapter 3.1, covers the same ground.

Exercises

Exercise 1 · write code

Make it changeable

This program does not compile (error E0384). Fix it with the smallest change so it prints:

score: 10
score: 15

File: main.rs

Starting point

fn main() {
    let score = 10;
    println!("score: {score}");
    score = score + 5;
    println!("score: {score}");
}

Example

expected output

score: 10
score: 15
Hint 1

Read the help line of the error. One word is missing from the let.

Reference solution
fn main() {
    let mut score = 10;
    println!("score: {score}");
    score = score + 5;
    println!("score: {score}");
}

Exercise 2 · write code

Shadowing to convert

A distance arrives in kilometers. Using shadowing (a second let with the same name, no mut), convert it to meters and then to centimeters, printing after each step:

distance = 3 km
distance = 3000 m
distance = 300000 cm

Start from let distance = 3; and do not use mut anywhere.

File: main.rs

Starting point

fn main() {
    let distance = 3;
    println!("distance = {distance} km");
}

Example

expected output

distance = 3 km
distance = 3000 m
distance = 300000 cm
Hint 1

let distance = distance * 1000; creates a new distance from the old one.

Reference solution
fn main() {
    let distance = 3;
    println!("distance = {distance} km");
    let distance = distance * 1000;
    println!("distance = {distance} m");
    let distance = distance * 100;
    println!("distance = {distance} cm");
}

Exercise 3 · quiz

Shadowing in a block

What does this program print?

fn main() {
    let x = 1;
    {
        let x = x + 10;
        println!("{x}");
    }
    println!("{x}");
}
Answer
  • 11 then 1

The inner let x creates a new variable that exists only until the closing brace of the block. It hides (shadows) the outer x inside the block but does not change it. After the block, the name x refers to the outer variable again, which is still 1. Assignment with mut would have changed the one and only x; shadowing never changes anything, it introduces a new name.

Exercise 4 · write code

Constants

Define two constants at the top level of the file (outside main):

both of type u32, and a third, SECONDS_PER_DAY, computed from them (times 24). Then print:

86400 seconds in a day

File: main.rs

Starting point

fn main() {
    println!("{} seconds in a day", SECONDS_PER_DAY);
}

Example

expected output

86400 seconds in a day
Hint 1

Constants need a type: const NAME: u32 = value;

Hint 2

A constant may be computed from other constants, because the compiler can evaluate it.

Reference solution
const SECONDS_PER_MINUTE: u32 = 60;
const MINUTES_PER_HOUR: u32 = 60;
const SECONDS_PER_DAY: u32 = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * 24;

fn main() {
    println!("{} seconds in a day", SECONDS_PER_DAY);
}

Exercise 5 · quiz

Which ones compile?

Which of these snippets compile (as the body of main)? Select all that apply.

Answer
  • let a = 5; let a = "five";

  • let d; d = 7; println!("{d}");

(a) Shadowing creates a new variable, which may have a different type. (b) b is immutable, error E0384. (c) mut lets you change the value, never the type: E0308 mismatched types. (d) A variable may be declared first and initialized later, as long as it is assigned exactly once before use; the compiler checks that on every path.