Skip to Content
Course content

15: References and Borrowing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a pattern with almost everyone I've mentored moving from C++ or Java to Rust. They usually start with this mindset: "References are just pointers that don't take ownership, so I can just pass them around and use them whenever I want."

In those other languages, that's mostly true. But in Rust, that way of thinking will lead you straight into a wall of compiler errors. Let me show you exactly why that's wrong with a piece of code that looks perfectly reasonable but will refuse to compile.

let mut players = vec!["Alice", "Bob", "Charlie"];

for player in &players {
    if *player == "Alice" {
        players.push("Guest"); // 💥 Error: cannot borrow `players` as mutable because it is also borrowed as immutable
    }
}

If you're thinking, "Why? I'm just adding a name to the end of the list," you're thinking like a C++ programmer. To the Rust compiler, this is a disaster waiting to happen. While you are iterating (borrowing the list immutably), you're trying to modify that same list (borrowing it mutably). If adding "Guest" causes the vector to reallocate its memory to a new location, the reference the loop is currently holding would suddenly point to deleted memory. Rust stops this before it ever happens.

The "Pointers are Free" Myth vs. The Borrowing Contract

The core shift you need to make is this: a reference in Rust isn't just a memory address; it's a contract. When you create a reference, you aren't just pointing to data—you are locking that data into a specific state for as long as that reference exists.

There are two types of these contracts:

  • Immutable Borrowing (&T): You're saying, "I need to read this, and I promise that as long as I'm looking at it, nobody will change it." You can have as many of these as you want simultaneously.
  • Mutable Borrowing (&mut T): You're saying, "I need to change this, and I demand that I am the only person with access to it right now."

I like to think of it as a library book. Lots of people can read the same book at once (immutable), but if someone wants to edit the text in the book (mutable), they have to take it home, and nobody else can even look at it until they bring it back.

The "One Writer or Many Readers" Rule

This is the golden rule of Rust borrowing. You can have either one mutable reference or any number of immutable references. You can never have both at the same time.

Let's fix that player list example from earlier. If we want to modify the list while checking its contents, we have to make sure the immutable borrow ends before the mutable borrow begins. I usually do this by collecting the changes first, or by using indices instead of references.

let mut players = vec!["Alice", "Bob", "Charlie"];
let mut to_add = Vec::new();

for player in &players {
    if *player == "Alice" {
        to_add.push("Guest");
    }
}

// The immutable borrow of `players` ended when the loop finished.
// Now we are free to borrow it mutably.
players.extend(to_add);

It feels like more work at first, but this is exactly how Rust eliminates "data races" at compile time. You'll stop worrying about whether a variable changed behind your back because the compiler literally won't let that happen.

Handling References in Functions

When you're designing your functions, you'll often wonder: "Should I take a reference or take ownership?" My rule of thumb is simple: start with a reference. If the function doesn't need to own the data to store it in a struct or send it to another thread, don't take ownership. It makes your API much more flexible for the caller.

struct Scoreboard {
    scores: Vec<i32>,
}

impl Scoreboard {
    // I use &self here because calculating the total shouldn't 
    // destroy the scoreboard or prevent others from reading it.
    fn total_score(&self) -> i32 {
        self.scores.iter().sum()
    }

    // I use &mut self because updating a score changes the state.
    fn update_score(&mut self, index: usize, value: i32) {
        self.scores[index] = value;
    }
}

Remember, the "scope" of a borrow lasts from the moment the reference is created until the last time it's used. Rust is actually quite smart about this (it's called Non-Lexical Lifetimes), so you don't always have to wrap everything in curly braces to "end" a borrow, but you still have to respect the One Writer/Many Readers rule.




📋 Practical Task

Fixing the Inventory Management System

You are building a simple RPG inventory system. Below is a broken piece of code. The programmer tried to create a function that checks if an item exists in the inventory and, if it does, logs a message using a reference to that item. However, they also tried to update the item's quantity at the same time, which is triggering the borrow checker.

Your Goal: Modify the process_item function so that it compiles. You must ensure that the immutable borrow (used for logging) is finished before the mutable borrow (used for updating the quantity) begins.

struct Item {
    name: String,
    quantity: u32,
}

struct Inventory {
    items: Vec<Item>,
}

impl Inventory {
    fn process_item(&mut self, item_name: &str) {
        // BUG: This loop creates an immutable borrow of self.items
        for item in &self.items {
            if item.name == item_name {
                println!("Found item: {} with quantity {}", item.name, item.quantity);
                
                // ERROR: Trying to borrow self mutably while the loop 
                // is still borrowing it immutably!
                self.update_quantity(item_name, 1);
            }
        }
    }

    fn update_quantity(&mut self, name: &str, amount: u32) {
        if let Some(item) = self.items.iter_mut().find(|i| i.name == name) {
            item.quantity += amount;
        }
    }
}

fn main() {
    let mut my_inv = Inventory {
        items: vec![
            Item { name: "Health Potion".to_string(), quantity: 5 },
            Item { name: "Iron Sword".to_string(), quantity: 1 },
        ],
    };

    my_inv.process_item("Health Potion");
    println!("New quantity: {}", my_inv.items[0].quantity);
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.