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 variablex
is the memory location holding5
. In such simple cases, thinking oflet
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
Static variables (static
) also represent values that live for the entire duration of the program ('static
lifetime), but unlike const
, they have a fixed, single memory address. Accessing a static
variable always reads from or writes to that specific location.
- Must have an explicit type annotation.
- Immutable statics (
static
) must be initialized with a constant expression (similar toconst
). - Mutable statics (
static mut
) exist but are inherently unsafe due to potential data races in concurrent programs. Accessing or modifying astatic mut
requires anunsafe
block. Their use is strongly discouraged in favor of safe concurrency primitives likeMutex
,RwLock
, or atomics (AtomicU32
, etc.). - Conventionally named using
SCREAMING_SNAKE_CASE
.
Note: By default, the latest Rust compiler refuses to compile code that uses static mut
variables:
// Immutable static: lives for the program duration at a fixed address. static APP_VERSION: &str = "1.0.2"; // Mutable static: requires unsafe to access (AVOID IF POSSIBLE). static mut REQUEST_COUNTER: u32 = 0; fn main() { println!("Running version: {}", APP_VERSION); // Accessing/modifying static mut requires unsafe block. // This is generally bad practice without proper synchronization. unsafe { REQUEST_COUNTER += 1; println!("Requests processed (unsafe): {}", REQUEST_COUNTER); } unsafe { REQUEST_COUNTER += 1; println!("Requests processed (unsafe): {}", REQUEST_COUNTER); } increment_safe_counter(); // Prefer safe alternatives increment_safe_counter(); } // 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): {}", SAFE_COUNTER.load(Ordering::Relaxed)); }
const
vs. static
:
- Use
const
when the value can be computed at compile time and you want it inlined directly into the code (no fixed address needed). - Use
static
when you need a single, persistent memory location for a value throughout the program’s lifetime (like a C global variable). Only usestatic mut
withinunsafe
blocks and with extreme caution, preferably replacing it with safe concurrency patterns.
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.