Borrowing and References

Tushar PamnaniTushar Pamnani
6 min read

Welcome back! In Chapter 1: Ownership, we learned about how Rust manages memory using the concept of ownership. We saw that when we pass a variable to a function, ownership is often moved, meaning the original variable can no longer be used. This can be inconvenient! What if we just want to let a function look at our data without taking it away? That's where borrowing and references come in.

Imagine you have a delicious cake (your data). You want to share it with a friend (a function), but you don't want to give them the whole cake! You just want them to have a slice (access to the data). Borrowing and references are like giving your friend a slice without giving up ownership of the entire cake. You still have the cake, and they can enjoy a piece.

What are Borrowing and References?

Borrowing is how Rust lets you access data owned by someone else, like borrowing a book from a friend. References are like bookmarks in the book, pointing to specific pages. There are two kinds of references:

  • Immutable references: These allow you to read the data but not change it. Think of it as borrowing a library book – you can read it, but you can't write in it.

  • Mutable references: These allow you to both read and write the data. This is like borrowing a book and being allowed to highlight parts or add notes.

Key Idea: You can have many immutable references, but you can only have one mutable reference at a time. This rule prevents different parts of your code from trying to change the same data at the same time, which could lead to unexpected results. It's like making sure only one person is editing a document at a time to avoid confusion!

Why Use Borrowing and References?

The main reason to use borrowing and references is to avoid moving ownership. This lets you use variables in multiple places without having to constantly transfer ownership back and forth. It's also more efficient, as you're not constantly copying data.

Let's look at a simple example:

fn main() {
    let my_string = String::from("hello");
    let len = calculate_length(&my_string); // Pass a reference

    println!("The length of '{}' is {}.", my_string, len);
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
}

Explanation:

  • We create a String called my_string.

  • We call the calculate_length function, passing &my_string. The & symbol creates a reference to my_string.

  • The calculate_length function takes a reference to a String (s: &String). This means the function can access the String data without taking ownership.

  • The function returns the length of the string.

  • Back in main, we can still use my_string because ownership was never transferred.

Output:

The length of 'hello' is 5.

If we tried to pass my_string directly to calculate_length, ownership would be moved, and we couldn't use my_string afterwards. Borrowing allows us to avoid this.

Immutable References

As we saw in the previous example, immutable references allow you to read data. You create an immutable reference using the & symbol.

fn main() {
    let my_number = 10;
    let reference_to_number = &my_number;

    println!("The original number is: {}", my_number);
    println!("The reference to the number is: {}", reference_to_number);
}

Explanation:

  • We create a variable my_number and assign it the value 10.

  • We create an immutable reference reference_to_number that points to my_number.

  • We can use both my_number and reference_to_number to access the value 10.

Output:

The original number is: 10
The reference to the number is: 10

You cannot modify the value through an immutable reference. This is important for ensuring data integrity.

Mutable References

Mutable references allow you to modify data. You create a mutable reference using the &mut symbol.

fn main() {
    let mut my_number = 10; // `mut` is important here!
    let mutable_reference = &mut my_number;

    *mutable_reference += 5; // Dereference and modify

    println!("The number is now: {}", my_number);
}

Explanation:

  • We create a mutable variable my_number (notice the mut keyword).

  • We create a mutable reference mutable_reference that points to my_number.

  • We use the * operator to dereference the reference, which allows us to access the actual value that the reference points to. We then add 5 to the value.

  • The original my_number is modified!

Output:

The number is now: 15

Important: You can only have one mutable reference to a particular piece of data at a time. Rust enforces this rule to prevent data races and other problems that can occur when multiple parts of your code are trying to modify the same data concurrently. Also, notice we need to declare my_number as mutable using the mut keyword. We will discuss Mutability more in the next chapter.

The Borrowing Rules in Action

Let's see the borrowing rules in action:

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

    let ref1 = &my_string; // Immutable borrow
    let ref2 = &my_string; // Another immutable borrow

    println!("ref1: {}, ref2: {}", ref1, ref2);

    let ref3 = &mut my_string; // Mutable borrow - this is OK because ref1 and ref2 are no longer used

    ref3.push_str(" world");

    println!("ref3: {}", ref3);
}

Explanation:

  • We can have multiple immutable borrows at the same time.

  • After the immutable borrows (ref1 and ref2) are no longer used, we can create a mutable borrow (ref3).

If we try to use ref1 or ref2 after creating ref3, the compiler will give us an error. Rust is making sure we don't accidentally read data that's being modified at the same time.

Now, let's see what happens if we try to have both mutable and immutable borrows simultaneously:

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

    let ref1 = &my_string;   // Immutable borrow
    let ref2 = &mut my_string; // Mutable borrow - ERROR!

    println!("ref1: {}", ref1);
    println!("ref2: {}", ref2);
}

This code will not compile. The compiler will tell you that you can't have a mutable borrow while you have an immutable borrow. This is a core principle of Rust's borrowing system, preventing data races and ensuring memory safety.

How Borrowing Works Under the Hood

Let's briefly explore what happens behind the scenes (simplified!). Remember how Chapter 1: Ownership mentioned the heap and the stack? When you create a reference, Rust essentially creates a pointer on the stack that points to the data on the heap.

Here's a simplified sequence diagram illustrating borrowing:

sequenceDiagram
    participant Main as main()
    participant Data as Heap ("hello")
    participant R1 as ref1
    participant R2 as ref2

    Main->>Data: Create String
    Data-->>Main: Ownership established

    Main->>R1: Create &reference
    R1-->>Data: Borrows (read-only)

    Main->>R2: Create &reference
    R2-->>Data: Borrows (read-only)

    Note right of R1: Immutable borrow
    Note right of R2: Another immutable borrow
    Note right of Main: main() still owns the data

Here's how the struct looks like (covered in Structs):

struct Reference<'a, T: ?Sized> {
    data: *const T, // Raw pointer to the data
    _marker: PhantomData<&'a T>, // PhantomData to store lifetime information
}

When you create a reference (immutable or mutable), Rust creates a pointer that refers to the data owned by another variable. This pointer stores the address of the data in memory, allowing you to access it without taking ownership. _marker field is important because it tracks the lifetime of the data.

Conclusion

Borrowing and references are essential tools in Rust for working with data without transferring ownership. By understanding the rules of borrowing (multiple immutable borrows or one mutable borrow at a time), you can write safe and efficient Rust code. We'll be discussing Mutability in the next chapter.

0
Subscribe to my newsletter

Read articles from Tushar Pamnani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tushar Pamnani
Tushar Pamnani

I am a MERN stack developer and a blockchain developer from Nagpur. I serve as Management Lead at ML Nagpur Community and The CodeBreakers Club, RCOEM.