Rust Variables and Mutability: Mastering the Basics with let and mut

Rust Variables and Mutability: Mastering the Basics with let and mut
Photo by Matt Artz / Unsplash

Rust is known for its safety-first approach, and one of the first things that sets it apart from other programming languages is its immutability by default. In Rust, variables are immutable unless explicitly declared otherwise, which helps prevent many common errors, especially in multi-threaded environments. Let’s dive into the details of Rust's variable system, exploring the let and mut keywords, immutability, and some other essential concepts that will lay a solid foundation for Rust programming.

1. Immutability by Default

In Rust, variables are immutable by default, meaning once you assign a value to a variable, you cannot change it. This default behavior is one of Rust’s core safety features. Here’s a simple example to illustrate this:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);

    // This line will throw an error because x is immutable
    x = 6;
}

Attempting to reassign x results in a compilation error:

error[E0384]: cannot assign twice to immutable variable `x`

The compiler prevents you from accidentally changing a variable’s value unless you explicitly allow it, making your code more predictable and less prone to bugs.

💡
Live demo here.

2. Using mut for Mutability

To create a mutable variable in Rust, you use the mut keyword. By marking a variable as mut, you’re telling Rust that you intend to change its value after the initial assignment. Here’s how it looks:

fn main() {
    let mut y = 10;
    println!("The initial value of y is: {}", y);

    y = 20;
    println!("The new value of y is: {}", y);
}

In this case, Rust allows you to reassign y because it’s declared as mutable with mut. Using mut is a way to opt-in to mutability, which makes your intention clear and helps others understand which parts of the code might change.

💡
Live demo here.

3. Shadowing: Rebinding a Variable

Rust also allows you to rebind a variable to a new value by "shadowing" it, which is distinct from mutability. Shadowing lets you reuse the same variable name, but effectively treat it as a new variable. This can be particularly useful when you need to transform data and assign it to the same name without making the variable mutable.

fn main() {
    let z = 15;
    let z = z + 1; // Shadowing: z is now 16
    let z = z * 2; // Shadowing again: z is now 32

    println!("The final value of z is: {}", z); // Output: 32
}

In this example, the let z = z + 1 statement creates a new z, rather than modifying the old one. This makes it possible to modify the value without requiring mut, which can be cleaner in some cases, especially when performing transformations.

💡
Live demo here.

4. Variable Types and Type Inference

Rust uses type inference, so you don’t always have to explicitly specify the type of a variable. Rust will attempt to infer the type based on the value you assign to it. However, if the type is ambiguous or you want to be explicit, you can annotate the type:

fn main() {
    let count: i32 = 42; // Specified type
    let name = "Rust";   // Type inferred as &str
}

While type inference is convenient, sometimes specifying types can improve readability and help avoid potential issues, especially as your codebase grows.

💡
Live demo here.

5. Constants: Using const for Immutable Values

In addition to variables, Rust also supports constants, which are similar to immutable variables but with a few key differences:

  • Constants are declared using the const keyword.
  • They must be explicitly typed.
  • They can be set only to constant expressions (values that can be determined at compile time).
fn main() {
    const MAX_POINTS: u32 = 100_000;
    
    println!("{}", MAX_POINTS);
}

Constants are great for values that you know will never change throughout the program, like configuration values or magic numbers. They cannot be shadowed or made mutable, providing an extra level of reliability.

💡
Live demo here.

6. Using Variables with Ownership in Mind

Rust’s variable system is tightly coupled with its ownership model, which is crucial for memory safety. When you assign or pass variables in Rust, you’re often moving ownership unless the type implements the Copy trait (like simple integers). Here’s an example to show what happens with ownership:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership of s1 is moved to s2

    println!("{}", s1); // This would cause an error because s1 is no longer valid
}

In this case, s1 is no longer accessible after being assigned to s2, which prevents potential double-free errors. Learning the basics of ownership along with variables is essential to writing safe, efficient Rust code.

💡
Live demo here.

7. Scoping: Block-Level Variable Scoping

In Rust, variables follow lexical scoping rules. This means that variables are only valid within the block where they’re defined. Once a variable goes out of scope, Rust will automatically clean it up:

fn main() {
    {
        let temp = 50;
        println!("temp inside block: {}", temp);
    }
    // temp is no longer accessible here
}

The concept of scoping helps manage resources efficiently. In the example above, temp only exists within the inner block, and Rust deallocates it once the block ends.

💡
Live demo here.

Key Takeaways and Best Practices

  • Immutability by default: Rust’s immutability by default encourages safer and more predictable code. Use the mut keyword only when you need a variable to be mutable.
  • Shadowing for transformations: If you want to “reuse” a variable name without mutability, shadowing can be a useful technique.
  • Constants for unchanging values: Use const for values that should never change. Constants are globally accessible and always immutable.
  • Understanding ownership and scoping: Rust’s ownership model ties closely to how variables work, especially with non-primitive data types like String.

Finally

Rust’s approach to variables and immutability might seem strict at first, but it’s there to help you write safe and performant code. Embracing these fundamentals will make your Rust journey smoother and your programs more robust. Remember, Rust encourages thinking about variable usage and changes explicitly—a practice that can help prevent bugs and lead to better code in any language.

Next we will learn about data types and functions in Rust.

Support Us