1. rust
  2. /basics
  3. /variables-mutability

Variables & Mutability

Rust takes a unique approach to variables and mutability that helps prevent common programming errors while maintaining performance. By default, variables in Rust are immutable, but you can opt into mutability when needed.

Immutable Variables by Default

In Rust, variables are immutable by default, meaning once you assign a value, you cannot change it:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    
    // x = 6; // This would cause a compile error
    // error[E0384]: cannot assign twice to immutable variable `x`
}

This immutability-by-default approach helps prevent bugs and makes code easier to reason about, especially in concurrent programs.

Making Variables Mutable

When you need to change a variable's value, you can make it mutable using 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);
}

When to Use Mutability

Use mutable variables when:

  • You need to modify the value after initialization
  • Building up data structures incrementally
  • Performance requires in-place modification
  • Working with iterative algorithms
fn main() {
    let mut numbers = Vec::new();
    
    // Building up a vector
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);
    
    println!("Numbers: {:?}", numbers);
    
    // Modifying existing values
    let mut counter = 0;
    for i in 1..=10 {
        counter += i;
    }
    println!("Sum: {}", counter);
}

Constants

Constants are always immutable and are declared using the const keyword:

const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;

fn main() {
    println!("Maximum points: {}", MAX_POINTS);
    println!("Pi: {}", PI);
}

Constants vs Immutable Variables

ConstantsImmutable Variables
Always immutableImmutable by default
Must be constUse let
Type must be annotatedType can be inferred
Can only be set to constant expressionsCan be set to runtime values
Valid for entire program runValid within their scope
const SECONDS_IN_HOUR: u32 = 60 * 60; // Constant expression - OK

fn main() {
    let current_time = std::time::SystemTime::now(); // Runtime value - OK for let
    // const NOW: std::time::SystemTime = std::time::SystemTime::now(); // Error!
}

Variable Shadowing

Rust allows you to declare a new variable with the same name as a previous variable:

fn main() {
    let x = 5;
    let x = x + 1;  // Shadows the previous x
    let x = x * 2;  // Shadows again
    
    println!("The value of x is: {}", x); // 12
}

Shadowing vs Mutability

Shadowing is different from making a variable mutable:

fn main() {
    // Shadowing allows type changes
    let spaces = "   ";           // string
    let spaces = spaces.len();    // number
    
    // Mutability doesn't allow type changes
    let mut spaces_mut = "   ";
    // spaces_mut = spaces_mut.len(); // Error: mismatched types
}

Practical Shadowing Examples

Shadowing is useful for transforming values:

fn main() {
    // Reading and parsing user input
    let input = "42";
    let input: i32 = input.trim().parse().expect("Not a number");
    
    // Transforming data through multiple steps
    let data = "hello world";
    let data = data.to_uppercase();
    let data = data.replace(" ", "_");
    
    println!("Transformed data: {}", data); // HELLO_WORLD
}

Scope and Lifetime

Variables are valid within the scope where they're declared:

fn main() {
    let outer = 10;
    
    {
        let inner = 20;
        println!("Inner scope: {} {}", outer, inner); // Both accessible
        
        let outer = 30; // Shadows the outer variable within this scope
        println!("Shadowed outer: {}", outer); // 30
    } // inner goes out of scope here
    
    println!("Back to outer scope: {}", outer); // 10 (original value)
    // println!("{}", inner); // Error: inner is not in scope
}

Block Scope

Variables declared in blocks have limited scope:

fn main() {
    let condition = true;
    
    let number = if condition {
        let temp = 5;
        temp * 2 // 10
    } else {
        let temp = 3;
        temp * 2 // 6
    };
    
    println!("Number: {}", number);
    // println!("{}", temp); // Error: temp is not in scope
}

Ownership and Variables

Variables in Rust have ownership over their data:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership moves from s1 to s2
    
    // println!("{}", s1); // Error: s1 no longer owns the data
    println!("{}", s2); // This works
}

Copy vs Move Semantics

Some types are copied instead of moved:

fn main() {
    // Types that implement Copy are copied
    let x = 5;
    let y = x; // x is copied to y
    println!("x: {}, y: {}", x, y); // Both are valid
    
    // Types that don't implement Copy are moved
    let s1 = String::from("hello");
    let s2 = s1; // s1 is moved to s2
    // println!("{}", s1); // Error: s1 has been moved
}

References and Borrowing

You can use references to access data without taking ownership:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // Borrow s1
    
    println!("The length of '{}' is {}.", s1, len); // s1 is still valid
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but doesn't drop the data because it's a reference

Mutable References

You can create mutable references to change borrowed data:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s); // "hello, world"
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Best Practices

1. Prefer Immutability

Start with immutable variables and only add mut when necessary:

// Good: Clear intent
let name = "Alice";
let mut counter = 0;

// Avoid: Unnecessary mutability
// let mut name = "Alice"; // name never changes

2. Use Meaningful Names

Choose descriptive variable names:

// Good
let user_count = 42;
let is_authenticated = true;
let maximum_retries = 3;

// Avoid
let n = 42;
let flag = true;
let max = 3;

3. Minimize Scope

Declare variables as close to their use as possible:

fn process_data() {
    // ... other code ...
    
    if needs_processing {
        let processed_data = expensive_computation(); // Declare when needed
        use_data(processed_data);
    }
}

4. Use Shadowing for Transformations

Leverage shadowing for data transformations:

fn parse_input(input: &str) -> Result<i32, std::num::ParseIntError> {
    let input = input.trim();        // Remove whitespace
    let input = input.parse::<i32>()?; // Parse to integer
    Ok(input)
}

Common Patterns

Builder Pattern with Mutability

struct Config {
    host: String,
    port: u16,
    timeout: u64,
}

impl Config {
    fn new() -> Self {
        Config {
            host: "localhost".to_string(),
            port: 8080,
            timeout: 30,
        }
    }
    
    fn host(mut self, host: String) -> Self {
        self.host = host;
        self
    }
    
    fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }
}

fn main() {
    let config = Config::new()
        .host("example.com".to_string())
        .port(9000);
}

Accumulator Pattern

fn sum_even_squares(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &num in numbers {
        if num % 2 == 0 {
            sum += num * num;
        }
    }
    sum
}

Debugging Variables

Use debug printing to inspect variables:

fn main() {
    let x = 5;
    let y = 10;
    
    println!("x = {}, y = {}", x, y);
    println!("x = {:#?}", x); // Pretty debug print
    
    // Using dbg! macro
    let z = dbg!(x + y); // Prints and returns the value
}

Understanding variables and mutability is fundamental to writing effective Rust code. Rust's approach of immutability by default, combined with explicit mutability and ownership rules, helps create safer and more maintainable programs while maintaining excellent performance.