5.4 Variables and Mutability
Rust variables are named references to memory that can store data of a specific type. By default, Rust variables are immutable, which often leads to safer and more predictable code.
5.4.1 Declaring Variables
You must declare a variable before using it. For example:
#![allow(unused)] fn main() { let x = 5; println!("x = {}", x); }
Here, x
is an immutable variable whose type is inferred as i32
. In Rust, we often say that the value 5
is bound to the variable x
. However, for primitive types, this binding is essentially just copying the value into x
’s storage. There is no distinct “value object” and “variable” that remain linked; rather, the value is directly stored in the variable itself.
5.4.2 Type Annotations and Inference
You can specify a type explicitly:
#![allow(unused)] fn main() { let x: i32 = 10; }
Alternatively, you can rely on inference:
#![allow(unused)] fn main() { let y = 20; // Inferred as i32 }
The compiler may infer other integer types if the context demands it (e.g., usize
for indexing an array).
5.4.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.4.4 Why Immutability by Default?
Disallowing accidental modification helps eliminate a common source of bugs and makes concurrency safer. Since immutable data can be shared freely, Rust can handle such data across threads without requiring locks or other synchronization.
5.4.5 Uninitialized Variables
Rust forbids the use of uninitialized variables. You can declare a variable first and initialize it later, provided that every possible execution path initializes the variable before any 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 permitted. You must initialize all fields or elements.
5.4.6 Constants
Constants do not change throughout a program’s execution. They must have:
- An explicitly declared type.
- A compile-time-known value (no runtime computation allowed).
They are declared with the const
keyword:
const MAX_POINTS: u32 = 100_000; fn main() { println!("Max = {}", MAX_POINTS); }
Because constants are fully determined 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 replicated or removed entirely if the optimizer determines they are unnecessary.
When to Use const
- If the value is always the same and must be known at compile time (e.g., for array sizes, math constants, buffer capacities).
- If the value does not need a fixed memory address at runtime.
- If your primary concern is letting the compiler optimize and inline without storing data in memory.
5.4.7 Static Variables
Static variables, declared with static
, have several distinguishing features:
- They occupy a single, fixed address in memory (often in the data or BSS segment).
- They persist for the entire program runtime.
- They require an explicit type and must be initialized with a compile-time-constant expression if it is an immutable static. (For certain complex types or runtime values, separate rules apply, but typically the data is still stored in one consistent memory location.)
#![allow(unused)] fn main() { static GREETING: &str = "Hello, world!"; }
In contrast to constants, static items always have a dedicated storage location:
- Accessing a static variable reads from (or writes to) that specific memory address.
- Even an immutable static is stored at a fixed address, rather than being inlined by the compiler.
Mutable Static Variables
static mut
allows mutable global data. However, because multiple threads might access it simultaneously, modifying a static mut
requires an unsafe
block to acknowledge the potential data races or other concurrency hazards:
#![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 absolutely necessary and carefully managed (e.g., with synchronization primitives).
When to Use static
- If you need a consistent memory address throughout the program’s execution (e.g., for low-level operations, FFI with C code that expects a data symbol, or truly global data).
- If you need a single shared instance of a value (mutable or immutable) that must outlive all other scopes.
- If the value might be large or complex such that referencing the same location makes more sense than inlining it multiple times.
By choosing between const
and static
, you decide whether the compiler should treat your data as a purely compile-time constant (which it can inline or remove as it sees fit) or as a memory-resident item with a single address. For most compile-time-known values, const
is ideal. Use static
when you need a unique, addressable object in memory.
5.4.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 requires unsafe
because of the potential for race conditions (the same issue exists in C, though without the added safety warnings). Rust typically encourages alternatives like OnceLock
from the standard library, which safely manages one-time initialization.
/// Safety: 'call_many' must never be called concurrently from multiple threads,
/// and 'expensive_call' must not call 'call_many' internally.
unsafe fn call_many() -> u32 {
static mut VALUE: u32 = 0;
if VALUE == 0 {
VALUE = expensive_call();
}
VALUE
}
5.4.9 Shadowing and Re-declaration
Shadowing occurs when you declare a new variable with the same name as an existing one. This can happen either:
-
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); }
-
In the same scope, by using
let
again with the same variable name. In this case, the older binding is overshadowed in all subsequent code. A common pattern is transforming a variable’s value while reusing its name:fn main() { let spaces = " "; // Here we create a new 'spaces' variable by shadowing the old one let spaces = spaces.len(); println!("Number of spaces: {}", spaces); }
This can be useful to avoid creating additional variable names for similar concepts or stages of data processing. Note, however, that re-borrowing or mutating an existing variable is different from shadowing. A shadowed variable is essentially a new variable.
5.4.10 Scopes and Deallocation
A variable’s scope starts at its declaration and ends at 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.4.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); }