5.5 Variables and Mutability

Rust variables serve as named references to memory that hold data of a specific type. By default, Rust variables are immutable, which promotes safer, more predictable code.

5.5.1 Declaring Variables

You must declare a variable before using it:

#![allow(unused)]
fn main() {
let x = 5;
println!("x = {}", x);
}

Here, x is inferred as i32. In Rust, we say that the value 5 is bound to x. For primitive types, this just copies the value into x’s storage; there is no separate “object” that remains linked.

5.5.2 Type Annotations and Inference

You can specify a type explicitly:

#![allow(unused)]
fn main() {
let x: i32 = 10;
}

Or rely on inference:

#![allow(unused)]
fn main() {
let y = 20; // Inferred as i32
}

If the context demands a specific type (e.g., usize for indexing), Rust will infer it accordingly.

5.5.3 Mutable Variables

Use mut to allow a variable’s value to change:

fn main() {
    let mut z = 30;
    println!("Initial z: {}", z);
    z = 40;
    println!("New z: {}", z);
}

5.5.4 Why Immutability by Default?

Prohibiting accidental modification helps eliminate a common source of bugs and makes concurrency safer. Since immutable data can be shared freely, Rust can handle it across threads without requiring additional synchronization.

5.5.5 Uninitialized Variables

Rust forbids using uninitialized variables. You can declare a variable first and initialize it later, as long as every possible execution path assigns a value before use:

fn main() {
    let a;
    let some_condition = true; // Simplified for example
    if some_condition {
        a = 42; // Must be initialized on this branch
    } else {
        a = 64; // Must be initialized on this branch as well
    }
    println!("a = {}", a);
}

Partial initialization of tuples, arrays, or structs is not allowed; you must initialize all fields or elements.

5.5.6 Constants

Constants never change during a program’s execution. They must have:

  • An explicitly declared type.
  • A compile-time-known value (no runtime computation).

They are declared with the const keyword:

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("Max = {}", MAX_POINTS);
}

Because constants are known at compile time, the compiler may optimize them aggressively:

  • They can be inlined wherever they are used.
  • They may occupy no dedicated storage at runtime.
  • They can be duplicated or removed entirely if the optimizer deems it necessary.

When to Use const

  1. The value is always the same and must be known at compile time (e.g., array sizes, math constants, or buffer capacities).
  2. The value does not need a fixed memory address at runtime.
  3. You want maximum flexibility for compiler optimization and inlining, without requiring extra memory storage.

5.5.7 Static Variables

Static variables, declared with static, have specific characteristics:

  1. They occupy a single, fixed address in memory (typically in the data or BSS segment).
  2. They persist for the entire program runtime.
  3. They require an explicit type and generally must be initialized with a compile-time-constant expression if immutable. (Certain more complex scenarios exist, but the data still resides at one fixed location.)
#![allow(unused)]
fn main() {
static GREETING: &str = "Hello, world!";
}

Unlike constants, static items always have a dedicated storage location:

  • Accessing a static variable reads or writes that specific memory address.
  • Even immutable static data occupies a fixed address, rather than being inlined by the compiler.

Mutable Static Variables

static mut allows mutable global data. However, since multiple threads could access it simultaneously, modifying a static mut variable requires an unsafe block to acknowledge potential data races:

#![allow(unused)]
fn main() {
static mut COUNTER: u32 = 0;

fn increment_counter() {
    unsafe {
        COUNTER += 1;
    }
}
}

In general, global mutable state is discouraged in Rust unless it is both necessary and carefully managed (e.g., with synchronization primitives).

When to Use static

  1. You need a consistent memory address for the item throughout the program’s execution (e.g., for low-level operations or FFI with C code expecting a data symbol).
  2. You need a single shared instance of something (mutable or immutable) that must outlive all other scopes.
  3. The item might be large or complex enough that referencing it in a single location makes more sense than duplicating it.

5.5.8 Static Local Variables

In C, you can have a local static variable inside a function that retains its value across calls. Rust can mimic this pattern, but it involves unsafe due to potential race conditions (the same issue exists in C, but C has fewer safety checks). Rust encourages higher-level alternatives like OnceLock (in the standard library), which safely handles one-time initialization.

/// Safety: 'call_many' must never be called concurrently from multiple threads,
///         and 'expensive_call' must not invoke 'call_many' internally.
unsafe fn call_many() -> u32 {
    static mut VALUE: u32 = 0;
    if VALUE == 0 {
        VALUE = expensive_call();
    }
    VALUE
}

5.5.9 Shadowing and Re-declaration

Shadowing occurs when you declare a new variable with the same name as an existing one. This can happen in two ways:

  1. In an inner scope, overshadowing the variable from the outer scope until the inner scope ends:
    fn main() {
        let x = 10;
        println!("Outer x = {}", x);
        {
            let x = 20;
            println!("Inner x = {}", x);
        }
        println!("Outer x again = {}", x);
    }
  2. In the same scope, by using let again with the same variable name. The older binding is overshadowed in all subsequent code. A common pattern is transforming a variable’s type while reusing its name:
    fn main() {
        let spaces = "   ";
        // Create a new 'spaces' by shadowing the old one
        let spaces = spaces.len();
        println!("Number of spaces: {}", spaces);
    }

In the above example, the original spaces was a string slice, while the new spaces is a numeric value. Shadowing can help you avoid creating extra variable names for data that evolves during processing. Remember that mutating an existing variable differs from shadowing: a shadowed variable is effectively a new binding.

5.5.10 Scopes and Deallocation

A variable’s scope begins at its declaration and ends at the close of the block in which it is declared:

fn main() {
    let b = 5;
    {
        let c = 10;
        println!("b={}, c={}", b, c);
    }
    // 'c' is out of scope here
    println!("b={}", b);
}

When a variable goes out of scope, Rust automatically drops it (calling its destructor if applicable).

5.5.11 Declaring Multiple Items

Rust typically uses one let per variable. However, you can destructure a tuple if you want to bind multiple values at once:

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