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
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 toconst
. - Mutable statics (
static mut
) exist but are inherently unsafe due to the risk of data races in concurrent programs. Any access or modification of astatic mut
requires anunsafe
block. Their use is strongly discouraged in favor of safe concurrency primitives such asMutex
,RwLock
, or atomic types likeAtomicU32
. - 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:
-
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 anunsafe
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. -
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
orlib.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 usestatic mut
withinunsafe
blocks and with extreme caution, preferably replacing it with safe concurrency patterns likestd::sync::Mutex
orstd::sync::atomic
types. As of Rust 2024, taking references tostatic 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.