Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5.5 Variables and Mutability

Variables associate names with data stored in memory.

5.5.1 Declaring Variables

Use the let keyword to declare a variable and initialize it.

#![allow(unused)]
fn main() {
let message = "Hello"; // Declare 'message', initialize it with "Hello"
let count = 10;       // Declare 'count', initialize it with 10
}

A Note on Terminology: “Binding”

You will frequently encounter the term “binding” in Rust literature (e.g., “variable binding,” “let binds a value to a name”). This term emphasizes that let creates an association between a name and a value or memory location.

While accurate, especially when discussing immutability, shadowing, or references, the term “binding” might feel slightly abstract for simple cases like let x: i32 = 5; if you’re used to C’s model where the variable x is the memory location holding 5. In such simple cases, thinking of let as declaring a variable and initializing it with a value is perfectly valid and perhaps more direct.

This chapter will often use simpler terms like “declare,” “initialize,” “assign,” or “holds a value” for basic variable operations, while reserving “binding” for contexts like immutability or shadowing where it adds clarity. Be aware that other Rust resources heavily use “binding” in all contexts.

5.5.2 Immutability by Default

By default, variables declared with let are immutable. Once initialized, their value cannot be changed.

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    // x = 6; // Compile Error: cannot assign twice to immutable variable `x`
}

This design choice encourages safer code by preventing accidental modifications and making program state easier to reason about, especially important for concurrency. We refer to let x = 5; as creating an immutable binding.

5.5.3 Mutable Variables

To allow a variable’s value to be changed after initialization, declare it using let mut.

fn main() {
    let mut y = 10;
    println!("The initial value of y is: {}", y);
    y = 11; // OK, because y was declared as mutable
    println!("The new value of y is: {}", y);
}

Use mut deliberately when you need to change a variable’s value. Prefer immutability when possible.

5.5.4 Type Annotations and Inference

Rust’s compiler features powerful type inference. It can usually determine the variable’s type automatically based on the initial value and how the variable is used later.

#![allow(unused)]
fn main() {
let inferred_integer = 42;       // Inferred as i32 (default integer type)
let inferred_float = 2.718;     // Inferred as f64 (default float type)
}

However, you can (and sometimes must) provide an explicit type annotation using a colon (:) after the variable name.

#![allow(unused)]
fn main() {
let explicit_float: f64 = 3.14;  // Explicitly typed as f64
let count: u32 = 0;              // Explicitly typed as u32

// Annotation needed when type isn't clear from initializer or context
let guess: u32 = "42".parse().expect("Not a number!");

// Annotation needed if declared without immediate initialization
let later_initialized: i32;
later_initialized = 100; // OK now
}

Annotations are required when the compiler cannot uniquely determine the variable’s type from its initialization and usage context (a common example is with functions like parse() which can return different types based on the annotation).

5.5.5 Uninitialized Variables

Rust guarantees, through compile-time checks, that you cannot use a variable before it has been definitely initialized on all possible code paths.

fn main() {
    let x: i32; // Declared but not initialized

    let condition = true;
    if condition {
        x = 1; // Initialized on this path
    } else {
        // If we comment out the line below, the compiler will complain
        // because 'x' might not be initialized before the println!.
        x = 2; // Initialized on this path too
    }

    // OK: The compiler knows 'x' is guaranteed to be initialized by this point.
    println!("The value of x is: {}", x);

    // let y: i32;
    // println!("{}", y); // Compile Error: use of possibly uninitialized variable `y`
}

This check eliminates a common source of bugs found in C/C++ related to reading uninitialized memory. Note that compound types like tuples, arrays, and structs must generally be fully initialized at once; partial initialization is usually not permitted for safe Rust code.

5.5.6 Constants

Constants represent values that are fixed for the entire program execution and are known at compile time. They are declared using the const keyword.

  • Must have an explicit type annotation.
  • Must be initialized with a constant expression – a value the compiler can determine without running the code (e.g., literals, simple arithmetic on other constants).
  • Conventionally named using SCREAMING_SNAKE_CASE.
  • Can be declared in any scope, including the global scope.
  • Are effectively inlined by the compiler wherever they are used. They don’t necessarily occupy a specific memory address at runtime.
const SECONDS_IN_MINUTE: u32 = 60;
const MAX_USERS: usize = 1000;

fn main() {
    let total_seconds = 5 * SECONDS_IN_MINUTE;
    println!("Five minutes is {} seconds.", total_seconds);

    let user_ids = [0u32; MAX_USERS]; // Use const for array size
    println!("Max users allowed: {}", MAX_USERS);
    println!("User ID array size: {}", user_ids.len());
}

Use const for values that are truly fixed, program-wide parameters or mathematical constants.

5.5.7 Static Variables

In Rust, global variables—that is, variables declared outside of any function—are called static variables. Like constants, static variables (static) represent values that exist for the entire duration of the program ('static lifetime). However, unlike const, they occupy a fixed, single memory address. Any access to a static variable involves reading from or writing to this specific memory location, similar to how global variables function in C.

Static variables:

  • Must have an explicit type annotation, just like const items.
  • Immutable statics (static) must be initialized with a constant expression, similar to const.
  • Mutable statics (static mut) exist but are inherently unsafe due to the risk of data races in concurrent programs. Any access or modification of a static mut requires an unsafe block. Their use is strongly discouraged in favor of safe concurrency primitives such as Mutex, RwLock, or atomic types like AtomicU32.
  • Are conventionally named using SCREAMING_SNAKE_CASE.

Important Note on Rust Edition 2024: References to static mut

With Rust Edition 2024, the compiler now defaults to disallowing shared or mutable references to static mut variables, even within unsafe blocks. This change stems from the static_mut_refs lint being set to deny by default. The rationale is that taking such references leads to immediate undefined behavior, even if the reference is never actually used. This occurs because maintaining Rust’s strict mutability XOR aliasing rule (either one mutable reference or many shared references, but not both) becomes exceptionally difficult with global mutable state, particularly in multithreaded or reentrant contexts.

Consider the following examples, which will now result in a compilation error in Rust Edition 2024:

#![allow(unused)]
fn main() {
static mut X: i32 = 23;
static mut Y: i32 = 24;
unsafe {
    let y = &X;             // ERROR: shared reference to mutable static
    let ref x = X;          // ERROR: shared reference to mutable static
    let (x, y) = (&X, &Y);  // ERROR: shared reference to mutable static
}

static mut NUMS: &[u8; 3] = &[0, 1, 2];
unsafe {
    println!("{NUMS:?}");   // ERROR: shared reference to mutable static
    let n = NUMS.len();     // ERROR: shared reference to mutable static
}
}

This error also applies to implicit references, such as those created when printing static mut variables or calling methods on them, as shown by the println! and len() examples above.

Workarounds for static mut References

For situations where a reference to a static mut variable is genuinely necessary—for instance, in specific embedded programming scenarios where the overhead of Mutex or atomics might be unacceptable—there are two primary workarounds:

  1. Using Raw Pointers: You can obtain a raw pointer to the static mut variable. Raw pointers in Rust do not come with the same aliasing guarantees as references and require manual dereferencing within an unsafe block.

    static mut GLOBAL_COUNTER: u32 = 0;
    
    fn main() {
        unsafe {
            // Obtain a raw mutable pointer
            let ptr: *mut u32 = &raw mut GLOBAL_COUNTER;
            *ptr += 1; // Dereference the raw pointer to modify
    
            // Obtain a raw constant pointer to read
            let const_ptr: *const u32 = &raw const GLOBAL_COUNTER;
            println!("COUNTER (via raw pointer): {}", *const_ptr);
        }
    }

    As illustrated, a raw pointer can be created from a static mut item using &raw mut or &raw const and then dereferenced to access the value. This explicitly signals to the compiler and other developers that you are operating outside of Rust’s usual reference safety guarantees.

  2. Allowing the Lint: You can explicitly disable the static_mut_refs lint for specific code sections or for the entire crate. This approach should be used with extreme caution, as it bypasses a critical safety check.

    To allow the lint for the entire crate, add the following attribute at the top of your main.rs or lib.rs file:

    #![allow(static_mut_refs)]
    
    static mut REQUEST_COUNTER: u32 = 0;
    
    fn main() {
        unsafe {
            // This will now compile due to the #![allow] attribute
            let ref_to_counter = &REQUEST_COUNTER;
            println!("Requests processed (unsafe with allowed lint): {}",
            ref_to_counter);
        }
    }

    While suppressing the error, this method does not eliminate the underlying undefined behavior. Therefore, it is only advisable when you have a profound understanding of memory safety and aliasing in your specific use case.

Here’s an example illustrating the proper use of static and safer alternatives to static mut:

// Immutable static: lives for the program duration at a fixed address.
static APP_VERSION: &str = "1.0.2";

// Mutable static: requires unsafe to access.
// As of Rust 2024, taking references to this is disallowed by default.
static mut REQUEST_COUNTER: u32 = 0;

fn main() {
    println!("Running version: {}", APP_VERSION);

    // Accessing/modifying static mut requires an unsafe block.
    // In Rust 2024, taking a direct reference like `&REQUEST_COUNTER`
    // would now result in a compilation error by default.
    unsafe {
        // Direct modification is still allowed within unsafe
        REQUEST_COUNTER += 1;

        // Not allowed in Rust 2024:
        // println!("Requests processed (unsafe direct access): {}", REQUEST_COUNTER);

        // Example of accessing via raw pointer (to bypass Rust 2024 reference lint)
        let raw_ptr_counter: *const u32 = &raw const REQUEST_COUNTER;
        println!("Requests processed (unsafe via raw pointer): {}", *raw_ptr_counter);
    }

    increment_safe_counter(); // Prefer safe alternatives
}

// A safer way to handle global mutable state using atomics
use std::sync::atomic::{AtomicU32, Ordering};
static SAFE_COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment_safe_counter() {
    // Atomically increment the counter
    SAFE_COUNTER.fetch_add(1, Ordering::Relaxed);
    println!("Requests processed (safe using atomics): {}",
    SAFE_COUNTER.load(Ordering::Relaxed));
}

Alternatives to mutable global variables are discussed later in Chapter 19, “Smart Pointers,” and the use of raw pointers is discussed in detail in Chapter 25, where we cover unsafe language extensions.

const vs. static

  • Use const when the value can be computed at compile time and you want it inlined directly into the code. These are similar to C macros for constants but include type checking.
  • Use static when you need a single, persistent memory location for a value throughout the program’s lifetime, much like a C global variable. Only use static mut within unsafe blocks and with extreme caution, preferably replacing it with safe concurrency patterns like std::sync::Mutex or std::sync::atomic types. As of Rust 2024, taking references to static mut is generally disallowed to prevent undefined behavior.

5.5.8 Shadowing

Rust allows you to declare a new variable with the same name as a previously declared variable within the same or an inner scope. This is called shadowing. The new variable declaration creates a new binding, making the previous variable inaccessible by that name from that point forward (or temporarily, within an inner scope).

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

    // Shadow x by creating a new variable also named x
    let x = x + 1; // This 'x' is a new variable, initialized using the old 'x'
    println!("Shadowed x = {}", x); // Prints 6

    {
        // Shadow x again in an inner scope
        let x = x * 2; // This is yet another 'x', local to this block
        println!("Inner shadowed x = {}", x); // Prints 12
    } // Inner scope ends, its 'x' binding disappears

    // We are back to the 'x' from the outer scope (the one holding 6)
    println!("Outer x after scope = {}", x); // Prints 6

    // Shadowing is often used to transform a value while reusing its name,
    // potentially even changing the type.
    let spaces = "   ";       // 'spaces' holds a &str (string slice)
    let spaces = spaces.len(); // The name 'spaces' is re-bound to a usize value
    println!("Number of spaces: {}", spaces); // Prints 3
}

Shadowing differs significantly from marking a variable mut. Mutating (let mut y = 5; y = 6;) changes the value within the same variable’s memory location, without changing its type. Shadowing (let x = 5; let x = x + 1;) creates a completely new variable (potentially with a different type) that happens to reuse the same name, making the old variable inaccessible by that name afterwards.

5.5.9 Scope and Lifetimes

A variable is valid (or “in scope”) from the point it’s declared until the end of the block {} in which it was declared. When a variable goes out of scope, Rust automatically calls any necessary cleanup code for that variable (this is part of the ownership and RAII system, detailed later).

fn main() { // Outer scope starts
    let outer_var = 1;
    { // Inner scope starts
        let inner_var = 2;
        println!("Inside inner scope: outer={}, inner={}", outer_var, inner_var);
    } // Inner scope ends, 'inner_var' goes out of scope and is cleaned up

    // println!("Outside inner scope: inner={}", inner_var);
    // Compile Error: `inner_var` not found in this scope
    println!("Back in outer scope: outer={}", outer_var);
} // Outer scope ends, 'outer_var' goes out of scope and is cleaned up

5.5.10 Declaring Multiple Variables (Destructuring)

While C allows int a, b;, Rust typically uses one let statement per variable. However, Rust supports destructuring assignment using patterns, which is often used with tuples or structs to initialize multiple variables at once.

fn main() {
    let (x, y) = (5, 10); // Destructure the tuple (5, 10)
                         // This binds x to 5 and y to 10

    println!("x={}, y={}", x, y);
}

We will see more advanced uses of patterns and destructuring later.