Rust - Variables & Mutability
Overview
Estimated time: 35–45 minutes
Learn how variables work in Rust, including immutability by default, the mut
keyword, constants, static variables, and variable shadowing. Understand Rust's unique approach to variable binding and memory safety.
Learning Objectives
- Understand Rust's immutable-by-default variable system.
- Learn when and how to use mutable variables with
mut
. - Distinguish between variables, constants, and static variables.
- Master variable shadowing and its use cases.
- Understand variable binding patterns and scope.
Prerequisites
Variables in Rust
Immutable by Default
In Rust, variables are immutable by default. Once a value is bound to a name, you can't change that value:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
// This would cause a compile error:
// x = 6; // Error: cannot assign twice to immutable variable
}
Expected Output:
The value of x is: 5
Making Variables Mutable
To make a variable mutable, use the mut
keyword:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6; // This is now allowed
println!("The value of x is: {}", x);
}
Expected Output:
The value of x is: 5
The value of x is: 6
Why Immutability by Default?
Safety and Concurrency
Immutable variables prevent many classes of bugs:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = calculate_sum(&numbers);
// We know 'numbers' hasn't been modified by calculate_sum
println!("Original numbers: {:?}", numbers);
println!("Sum: {}", sum);
}
fn calculate_sum(nums: &Vec) -> i32 {
// This function can't modify the original vector
nums.iter().sum()
}
Expected Output:
Original numbers: [1, 2, 3, 4, 5]
Sum: 15
Compiler Optimizations
Immutable variables enable aggressive compiler optimizations because the compiler knows values won't change.
Mutable Variables
When to Use Mutability
Use mutable variables when you need to modify values:
fn main() {
let mut counter = 0;
// Increment counter in a loop
for i in 1..=5 {
counter += i;
println!("Step {}: counter = {}", i, counter);
}
println!("Final counter: {}", counter);
}
Expected Output:
Step 1: counter = 1
Step 2: counter = 3
Step 3: counter = 6
Step 4: counter = 10
Step 5: counter = 15
Final counter: 15
Mutable References
You can pass mutable references to functions:
fn main() {
let mut name = String::from("Alice");
println!("Before: {}", name);
make_uppercase(&mut name);
println!("After: {}", name);
}
fn make_uppercase(s: &mut String) {
*s = s.to_uppercase();
}
Expected Output:
Before: Alice
After: ALICE
Constants
Declaring Constants
Constants are always immutable and must be annotated with their type:
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265359;
const APP_NAME: &str = "My Rust App";
fn main() {
println!("Maximum points: {}", MAX_POINTS);
println!("Pi value: {}", PI);
println!("App name: {}", APP_NAME);
}
Expected Output:
Maximum points: 100000
Pi value: 3.14159265359
App name: My Rust App
Constants vs Variables
Feature | Variables (let) | Constants (const) |
---|---|---|
Mutability | Can be mutable with mut |
Always immutable |
Type annotation | Optional (inferred) | Required |
Evaluation | Runtime | Compile time |
Scope | Block scope | Can be global |
Shadowing | Allowed | Not allowed |
Constant Expressions
Constants can only be set to constant expressions, not runtime values:
const SECONDS_IN_HOUR: u32 = 60 * 60; // OK: compile-time calculation
const GREETING: &str = "Hello, World!"; // OK: string literal
fn main() {
// const CURRENT_TIME: u64 = std::time::now(); // Error: not a constant expression
println!("Seconds in an hour: {}", SECONDS_IN_HOUR);
println!("Greeting: {}", GREETING);
}
Expected Output:
Seconds in an hour: 3600
Greeting: Hello, World!
Static Variables
Global Static Variables
Static variables have a 'static
lifetime and exist for the entire program duration:
static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static PROGRAM_NAME: &str = "Rust Tutorial";
fn main() {
println!("Program: {}", PROGRAM_NAME);
// Increment atomic counter
COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
println!("Counter: {}", COUNTER.load(std::sync::atomic::Ordering::SeqCst));
}
Expected Output:
Program: Rust Tutorial
Counter: 1
Mutable Static Variables
Mutable static variables require unsafe
code:
static mut UNSAFE_COUNTER: i32 = 0;
fn main() {
unsafe {
UNSAFE_COUNTER += 1;
println!("Unsafe counter: {}", UNSAFE_COUNTER);
}
}
Expected Output:
Unsafe counter: 1
Note: Mutable static variables are generally discouraged. Use atomic types or other thread-safe primitives instead.
Variable Shadowing
Basic Shadowing
You can declare a new variable with the same name as a previous variable:
fn main() {
let x = 5;
println!("First x: {}", x);
let x = x + 1; // Shadows the previous x
println!("Second x: {}", x);
let x = x * 2; // Shadows again
println!("Third x: {}", x);
}
Expected Output:
First x: 5
Second x: 6
Third x: 12
Shadowing with Different Types
Shadowing allows you to change the type of a variable:
fn main() {
let spaces = " "; // String slice
println!("Spaces as string: '{}'", spaces);
let spaces = spaces.len(); // Now it's a number
println!("Number of spaces: {}", spaces);
let spaces = spaces > 2; // Now it's a boolean
println!("More than 2 spaces: {}", spaces);
}
Expected Output:
Spaces as string: ' '
Number of spaces: 3
More than 2 spaces: true
Shadowing vs Mutability
Shadowing is different from mutability:
fn main() {
// Shadowing - creates new variables
let x = "hello";
let x = x.len(); // OK: different types
println!("Length: {}", x);
// Mutability - modifies existing variable
let mut y = "hello";
// y = y.len(); // Error: can't change type of mutable variable
y = "world"; // OK: same type
println!("New value: {}", y);
}
Expected Output:
Length: 5
New value: world
Variable Scope and Binding
Block Scope
Variables are scoped to the block where they're declared:
fn main() {
let outer = "I'm in main";
println!("{}", outer);
{
let inner = "I'm in a block";
println!("{}", inner);
println!("Can still access: {}", outer);
let outer = "I shadow the outer variable";
println!("Shadowed: {}", outer);
}
println!("Back to original: {}", outer);
// println!("{}", inner); // Error: 'inner' is not in scope
}
Expected Output:
I'm in main
I'm in a block
Can still access: I'm in main
Shadowed: I shadow the outer variable
Back to original: I'm in main
Function Parameter Scope
fn greet(name: &str) { // 'name' parameter is scoped to this function
let greeting = format!("Hello, {}!", name);
println!("{}", greeting);
}
fn main() {
let person = "Alice";
greet(person);
// 'greeting' is not accessible here
println!("Person: {}", person);
}
Expected Output:
Hello, Alice!
Person: Alice
Pattern Matching in Variable Binding
Destructuring Tuples
fn main() {
let tuple = (1, 2, 3);
let (x, y, z) = tuple; // Destructure into separate variables
println!("x: {}, y: {}, z: {}", x, y, z);
let (first, _, third) = tuple; // Ignore middle value
println!("first: {}, third: {}", first, third);
}
Expected Output:
x: 1, y: 2, z: 3
first: 1, third: 3
Destructuring Arrays
fn main() {
let array = [1, 2, 3, 4, 5];
let [first, second, rest @ ..] = array;
println!("First: {}", first);
println!("Second: {}", second);
println!("Rest: {:?}", rest);
}
Expected Output:
First: 1
Second: 2
Rest: [3, 4, 5]
Memory and Performance Implications
Stack vs Heap Allocation
fn main() {
// Stack allocated - fast, automatic cleanup
let x = 42; // i32 on stack
let array = [1, 2, 3]; // Array on stack
// Heap allocated - flexible size, manual management
let mut vec = Vec::new(); // Vector on heap
vec.push(1);
vec.push(2);
vec.push(3);
println!("Stack integer: {}", x);
println!("Stack array: {:?}", array);
println!("Heap vector: {:?}", vec);
}
Expected Output:
Stack integer: 42
Stack array: [1, 2, 3]
Heap vector: [1, 2, 3]
Copy vs Move Semantics
fn main() {
// Copy types (implement Copy trait)
let x = 5;
let y = x; // x is copied, both x and y are valid
println!("x: {}, y: {}", x, y);
// Move types (don't implement Copy)
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2, s1 is no longer valid
println!("s2: {}", s2);
// println!("s1: {}", s1); // Error: s1 was moved
}
Expected Output:
x: 5, y: 5
s2: hello
Best Practices
Prefer Immutability
- Start with immutable variables
- Add
mut
only when necessary - Consider if the design can avoid mutation
fn main() {
// Good: immutable when possible
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum(); // No mutation needed
// Better than mutating in a loop
// let mut sum = 0;
// for num in &numbers {
// sum += num;
// }
println!("Sum: {}", sum);
}
Use Descriptive Names
fn main() {
// Poor naming
let x = 42;
let y = x * 2;
// Good naming
let user_age = 42;
let double_age = user_age * 2;
println!("User age: {}, Double: {}", user_age, double_age);
}
Limit Variable Scope
fn main() {
let result = {
let temp_calculation = expensive_operation();
temp_calculation * 2 // Return value from block
};
// temp_calculation is no longer in scope
println!("Result: {}", result);
}
fn expensive_operation() -> i32 {
// Simulate expensive computation
42
}
Expected Output:
Result: 84
Common Pitfalls
Trying to Mutate Immutable Variables
fn main() {
let x = 5;
// x = 6; // Error: cannot assign twice to immutable variable
// Solution: use mut
let mut y = 5;
y = 6; // OK
println!("y: {}", y);
}
Confusing Shadowing with Mutation
fn main() {
let mut x = 5;
x = 6; // Mutation - changes the value
let x = 7; // Shadowing - creates new variable
// x = 8; // Error: new x is immutable
println!("x: {}", x);
}
Scope Confusion
fn main() {
let x = 5;
{
let x = 10; // Shadows outer x
println!("Inner x: {}", x);
}
println!("Outer x: {}", x); // Original x is restored
}
Expected Output:
Inner x: 10
Outer x: 5
Checks for Understanding
- What keyword makes a variable mutable in Rust?
- What's the difference between
let
andconst
? - Can you change the type of a mutable variable?
- What is variable shadowing?
- Where are constants evaluated - compile time or runtime?
Answers
mut
- placed before the variable namelet
creates variables (can be mutable),const
creates compile-time constants (always immutable)- No, you can only change the value, not the type. Use shadowing to change types
- Declaring a new variable with the same name as an existing variable, hiding the previous one
- Compile time - constants must be constant expressions