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

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

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

  1. What keyword makes a variable mutable in Rust?
  2. What's the difference between let and const?
  3. Can you change the type of a mutable variable?
  4. What is variable shadowing?
  5. Where are constants evaluated - compile time or runtime?

Answers

  1. mut - placed before the variable name
  2. let creates variables (can be mutable), const creates compile-time constants (always immutable)
  3. No, you can only change the value, not the type. Use shadowing to change types
  4. Declaring a new variable with the same name as an existing variable, hiding the previous one
  5. Compile time - constants must be constant expressions

← PreviousNext →