Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Understanding Borrowing: Immutable and Mutable References

Recommended resources

Hello! Let's dive back into Rust.

In our last lesson, we explored Rust's ownership model and saw how it enforces memory safety. We learned that when you pass an owned value like a String to a function, ownership is moved, and you can no longer use the original variable. This led to a critical question: how can we let a function use some data without giving it ownership? Constantly passing ownership back and forth, or cloning data everywhere, would be clumsy and inefficient.

Today, we'll answer that question by exploring borrowing and references. This is Rust's elegant solution for accessing data without taking ownership. You will learn how to use immutable references to read data and mutable references to modify it, all while satisfying the compiler's strict safety rules. Mastering this is the key to writing idiomatic and efficient Rust code.

1. The Problem with Moving: A Quick Recap

Remember this pattern from our last lesson?

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length) // Return the String so we can use it again
}

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

This works, but it's awkward. We had to return the String from the function just to give ownership back to the main function. What if we just wanted the function to read the string's length without consuming it?


2. Immutable References: Borrowing without Taking

This is where references come in. A reference is like a pointer in C, but with a lot more safety guarantees. It allows you to refer to some value without taking ownership of it. This is called borrowing.

Instead of passing a String to our function, we can pass a reference to a String, denoted by &String.

The video below gives an excellent overview of how references solve the ownership problem we just discussed.

Understanding Ownership in Rust

This clip from the 'Let's Get Rusty' channel directly addresses the cumbersome pattern of returning ownership and introduces references as the solution.

Watch from 12:06 to 14:27. Pay close attention to how the calculate_length function signature changes from s: String to s: &String and how the function is called using &s1.

As the video showed, we can fix our calculate_length function like this:

// The function now takes a reference to a String
fn calculate_length(s: &String) -> usize {
    s.len()
} // `s` goes out of scope here, but because it does not have ownership,
  // the String it refers to is NOT dropped.

fn main() {
    let s1 = String::from("hello");

    // We create a reference to s1 and pass it to the function.
    let len = calculate_length(&s1);

    // s1 is still valid here because we never moved ownership!
    println!("The length of '{}' is {}.", s1, len);
}

The ampersand & creates a reference. We call this an immutable reference. By default, references are immutable, meaning you can read the data they point to, but you can't change it.

The most important rule for immutable borrowing is:

You can have as many immutable references to a particular piece of data as you want, all at the same time.

This makes sense—if no one is changing the data, it's safe for many parts of your program to read it simultaneously.


3. Mutable References: Borrowing to Modify

What if we want a function to change a value? For that, we need a mutable reference, written as &mut.

To create a mutable reference, two things are required:

  1. The original variable must be declared with mut.
  2. You must use &mut when creating the reference.

Let's look at an example. Imagine we want a function that appends text to a string:

fn main() {
    // 1. The variable must be mutable.
    let mut s = String::from("hello");

    // 2. We pass a mutable reference.
    change(&mut s);

    println!("{}", s); // Prints "hello, world"
}

// The function takes a mutable reference.
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

This is incredibly powerful, but it comes with a strict rule, which is at the heart of Rust's prevention of data races:

You can have only one mutable reference to a particular piece of data in a given scope.

Furthermore, if you have a mutable reference, you cannot have any immutable references in that same scope. Think of it like this: if someone has permission to change the data, no one else should be reading it at the same time, because the data could change from under them.

The following article explains these rules with practical examples, including how they apply to struct methods, which will be very relevant for our chess game.

Rust Mutability, Moving and Borrowing - The Straight Dope - tutorials

The article 'Rust Mutability, Moving and Borrowing - The Straight Dope' gives a fantastic, down-to-earth explanation of mutable borrows and the compiler errors you'll hit if you break the rules.

Please read the following sections: What on earth is a "borrow?": This introduces the two types of borrows. Immutable Borrows: A quick review of what we just covered. Mutable Borrows: This is the core section. Pay close attention to the syntax (&mut) and the example of a change_name function. Note the explanation of the cannot borrow as mutable because it is also borrowed as immutable error. Other common pitfalls that will destroy you: This shows how borrowing works with methods on structs (&self and &mut self), which is a crucial concept for our project.

The diagram below provides a great visual summary of the concepts of moving, copying, and borrowing.

Rust Ownership, Move, Copy, and Borrowing Visualizations
This chart contrasts `move`, `copy`, and `borrow` semantics. The sections on mutable and immutable borrows summarize the rules we've just discussed.

4. Hands-On Activity: The Borrow Checker Is Your Friend

Time to put this into practice and fight the borrow checker! This is the best way to build an intuition for these rules. We'll create a simple struct for a chess player and write functions that borrow it.

  1. Open your rust_chess project and go to src/main.rs.

  2. Delete any existing code inside your main function.

  3. Define the following struct above main:

    struct ChessPlayer {
        username: String,
        elo: u32,
    }
    
  4. Immutable Borrow: Write a function show_player_stats that takes an immutable reference &ChessPlayer and prints the player's info.

    fn show_player_stats(player: &ChessPlayer) {
        println!("Player: {}, ELO: {}", player.username, player.elo);
    }
    
  5. Mutable Borrow: Now, write a function update_elo that takes a mutable reference &mut ChessPlayer and a u32 for the new ELO rating, and updates the player's elo.

    fn update_elo(player: &mut ChessPlayer, new_elo: u32) {
        player.elo = new_elo;
    }
    
  6. Putting it together: In your main function, write the following code to use your new functions:

    fn main() {
        let mut player1 = ChessPlayer {
            username: String::from("Magnus"),
            elo: 2830,
        };
    
        // Pass an immutable reference
        show_player_stats(&player1);
    
        // Pass a mutable reference
        update_elo(&mut player1, 2840);
        
        // Pass an immutable reference again to see the changes
        show_player_stats(&player1);
    
        // We can still use player1 here because we only borrowed it!
        println!("{}'s new rating is confirmed.", player1.username);
    }
    

    Run cargo run. Everything should compile and work as expected.

  7. Break the Rules! Now, let's see the borrow checker in action. Add the following lines to the end of your main function and try to compile. Read the compiler errors carefully—they are very helpful!

    • Error 1: Two mutable borrows.

      let r1 = &mut player1;
      let r2 = &mut player1; // ERROR!
      println!("{}, {}", r1.username, r2.username);
      

      The compiler will tell you cannot borrow 'player1' as mutable more than once at a time.

    • Error 2: Mutable and immutable borrows. (Comment out the previous error-causing lines first).

      let r1 = &player1; // immutable borrow
      let r2 = &mut player1; // mutable borrow - ERROR!
      println!("{}, {}", r1.username, r2.username);
      

      The compiler will complain that you cannot borrow 'player1' as mutable because it is also borrowed as immutable.

This exercise is fundamental. Understanding why this code fails is as important as knowing how to write code that succeeds.


Conclusion

Excellent work! You have now learned the core mechanics of borrowing, which is how Rust programs achieve both safety and efficiency. This is a huge step forward.

Key Takeaways:

  • Borrowing lets you access data using a reference without taking ownership.
  • Immutable references (&) provide read-only access. You can have many of them simultaneously.
  • Mutable references (&mut) provide read-write access. You can only have one of them in a given scope, and no immutable references can exist at the same time.
  • These rules are enforced at compile-time by the borrow checker to prevent data races.

We've seen that Rust defaults to moving ownership, and we can use borrowing to avoid that. But what if you actually want a full, independent copy of a heap-allocated value like a String or your ChessPlayer struct? In our next lesson, we will cover how to do this explicitly using the clone() method.