Module 1 • Lesson 4

Variables and Mutability

📚 8 min read 💻 Free Course 🦀 nixus.pro

Variables and Immutability by Default

One of Rust's most distinctive features is that variables are immutable by default. This is not an accident or a limitation - it is a deliberate design decision that prevents an entire class of bugs.

fn main() {
    let x = 5;
    println!("x = {x}");

    // This would NOT compile:
    // x = 6;  // ERROR: cannot assign twice to immutable variable

    // To make it mutable, use mut:
    let mut y = 5;
    println!("y = {y}");
    y = 6;  // This works!
    println!("y = {y}");
}

Why immutability by default? When you know a value will not change, the compiler can:

Rule of Thumb

Start with let (immutable). Only add mut when the compiler tells you the variable needs to change. This makes your code's intent explicit.

Constants

Constants are different from immutable variables. They are truly constant - computed at compile time, valid for the entire program lifetime, and can be used in any scope:

// Constants use SCREAMING_SNAKE_CASE
// Type annotation is required
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265358979;

// In a struct/impl context
const SPEED_OF_LIGHT: u64 = 299_792_458; // meters per second

fn main() {
    println!("Max points: {MAX_POINTS}");
    println!("Pi: {PI}");

    // Note: underscores in numeric literals improve readability
    let one_million = 1_000_000;
    let hex = 0xFF_AA_BB;
    let binary = 0b1111_0000;
    println!("{one_million}, {hex}, {binary}");
}

Constants vs immutable variables:

Shadowing

Rust allows you to shadow a variable by declaring a new one with the same name. This is different from mutation:

fn main() {
    let x = 5;
    println!("x = {x}");  // 5

    let x = x + 1;        // Shadow x with a new binding
    println!("x = {x}");  // 6

    {
        let x = x * 2;    // Shadow only in this block
        println!("x = {x}");  // 12
    }

    println!("x = {x}");  // 6 (inner shadow gone)

    // Key power: shadowing can change the type!
    let spaces = "   ";           // &str type
    let spaces = spaces.len();    // usize type
    println!("spaces: {spaces}"); // 3

    // With mut, you cannot change type:
    let mut spaces2 = "   ";
    // spaces2 = spaces2.len(); // ERROR: mismatched types
}

Shadowing is useful when you want to transform a value and give the result the same conceptual name, or when parsing input from one type to another.

Scope and Ownership Preview

Variables in Rust are scoped to their enclosing block. When they go out of scope, memory is automatically freed:

fn main() {
    // x is not yet valid here

    {
        let x = 5;  // x is valid from here
        println!("x = {x}");
    }   // x goes out of scope here, memory freed

    // x is not valid here - compiler error if you try to use it

    // Nested scopes
    let outer = "I'm in outer scope";
    {
        let inner = "I'm in inner scope";
        println!("{}", outer); // outer is accessible here
        println!("{}", inner);
    }
    println!("{}", outer); // Still valid
    // println!("{}", inner); // ERROR: inner not in scope
}

This automatic cleanup when a variable goes out of scope is called "drop" in Rust. It is the foundation of Rust's memory management system, which we will explore deeply in Module 2.

The let Statement: Full Power

fn main() {
    // Basic binding
    let a = 5;

    // With type annotation
    let b: i32 = 5;

    // Destructuring (more on this in Module 4)
    let (x, y, z) = (1, 2, 3);
    println!("x={x}, y={y}, z={z}");

    // Destructuring with underscore (ignore)
    let (first, _, last) = (1, 2, 3);
    println!("first={first}, last={last}");

    // let in if expression
    let number = 7;
    let description = if number % 2 == 0 { "even" } else { "odd" };
    println!("{number} is {description}");

    // Ignoring unused variables with underscore prefix
    let _unused_variable = 42;  // No warning from compiler
}

🎯 Practice Exercise

  1. Create a program that declares a temperature in Celsius as an immutable let
  2. Use shadowing to convert it to Fahrenheit (reassign with same name)
  3. Create a constant ABSOLUTE_ZERO_C: f64 = -273.15
  4. Print whether your temperature is above absolute zero
  5. Demonstrate that trying to mutate an immutable variable produces a compiler error, then fix it

🎉 Key Takeaways