iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Variables & Mutability

Variables in Rust: bindings, shadowing, mutability, constants, statics, type inference, and shadowing vs reassignment.

Rust — variables and bindings

EXAMPLE
// ===== let creates an immutable binding =====
fn main() {
    let count = 0;
    // count = count + 1;  // compile error: cannot assign twice to immutable

    // ===== mut for mutation =====
    let mut total = 0;
    total += 10;
    total += 5;
    println!("{}", total); // 15

    // ===== Type inference + annotations =====
    let x = 42;             // i32 inferred
    let y: u64 = 42;        // explicit
    let z = 1_000_000;      // underscores allowed in numeric literals
    let pi: f64 = 3.14;
    let on = true;
    let ch: char = 'Z';     // 4-byte Unicode scalar
    let name = String::from("Alex");

    // ===== Shadowing: a new binding with the same name =====
    let s = "42";
    let s: i32 = s.parse().expect("int");   // s now an i32; old binding shadowed
    let s = s + 1;                            // shadow again
    println!("{}", s);

    // Shadowing is NOT mutation: type can change, the new binding is fresh.

    // ===== const: compile-time constant =====
    const MAX_USERS: usize = 100;            // type required, must be const-evaluable
    println!("max = {}", MAX_USERS);

    // ===== static: program-lifetime memory =====
    static GREETING: &str = "hello";       // immutable static; takes a fixed address
    println!("{}", GREETING);

    // static mut exists but requires `unsafe` and is almost always the wrong tool.

    // ===== Scope =====
    let outer = 1;
    {
        let inner = outer + 1;
        println!("{}", inner);
    } // inner dropped here
    // println!("{}", inner); // error: inner not in scope

    // ===== Destructuring =====
    let (a, b) = (1, 2);
    let [first, .., last] = [10, 20, 30, 40];
    println!("{} {} {} {}", a, b, first, last);

    // ===== Pattern bindings in match =====
    let pair = (3, -7);
    match pair {
        (0, 0)                  => println!("origin"),
        (x, 0) | (0, x)         => println!("on axis at {}", x),
        (x, y) if x.abs() == y.abs() => println!("diagonal"),
        (x, y)                  => println!("point {},{}", x, y),
    }

    // ===== Strings: &str vs String =====
    let borrowed: &str = "static slice";    // string slice into static memory
    let owned: String = String::from("heap-allocated, growable");
    let _push: String = owned + " + more";  // String supports + (consumes owned)

    // ===== Move vs copy =====
    let owned1 = String::from("a");
    let owned2 = owned1;          // owned1 moved into owned2
    // println!("{}", owned1);    // error: value used after move

    let n1 = 42_i32;
    let n2 = n1;                  // i32 is Copy; both valid
    println!("{} {}", n1, n2);

    // ===== Why immutability by default =====
    // - Concurrency: shared, immutable data is data-race-free
    // - Optimiser: more aggressive when bindings are stable
    // - Code review: reassignment is rare and stands out when it appears

    // ===== Patterns to internalise =====
    // - Default to let, reach for let mut only where you genuinely mutate
    // - Use shadowing to evolve a value's type through a function (parse -> validate)
    // - const for things known at compile time; static rarely
    // - Destructure aggressively in let and match; smaller bindings read better
    // - Prefer &str function parameters; return String when you need ownership
}

// ===== Pitfalls =====
// - Forgetting mut and getting 'cannot borrow as mutable' -> add mut to the let
// - Shadowing inside an inner scope and being surprised when outer reverts
// - 'Variable' vs 'binding': move semantics mean the binding stops being valid
// - Using static mut: requires unsafe and is almost always wrong
// - const fn limits: not all expressions are const-evaluable yet

Why it matters

Immutable by default + shadowing + clear move semantics is what makes Rust feel rigorous. Reach for let, reach for mut only when you mean to mutate, and lean on shadowing when a value goes through transforming stages. The compiler stops bugs that other languages need tests to catch.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
let x = 5;          // immutable by default
let mut y = 10;     // mutable
const MAX: u32 = 100;
Try it Yourself »

Exercise

Make a variable mutable.

let count = 0;

Discussion

Loading…