Web Analytics

References & Borrowing

Intermediate ~25 min read

In the previous lesson, we learned that transferring ownership can be tedious. Imagine having to pass ownership back and forth with every function call! Fortunately, Rust provides references - a way to refer to a value without taking ownership of it. This is called borrowing.

What is a Reference?

A reference is like a pointer: it's an address we can follow to access data stored at that address. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

Real-World Analogy: Think of borrowing like checking out a library book. You can read it (immutable reference) or make notes in it if it's a special notebook (mutable reference), but you don't own it. When you're done, you return it to the library (the owner).
let s1 = String::from("hello");
let len = calculate_length(&s1);  // &s1 creates a reference to s1

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

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

The &s1 syntax creates a reference that refers to the value of s1 but does not own it. Because it doesn't own it, the value it points to will not be dropped when the reference stops being used.

Immutable References (&T)

By default, references are immutable. You can have multiple immutable references to the same data:

Output
Click Run to execute your code
Multiple Readers: Having multiple immutable references is safe because no one can modify the data. It's like having multiple people reading the same book - they don't interfere with each other.

Mutable References (&mut T)

If you want to modify a borrowed value, you need a mutable reference:

let mut s = String::from("hello");

change(&mut s);  // Pass a mutable reference

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
Important Restriction: You can have only one mutable reference to a particular piece of data at a time. This prevents data races at compile time!
Output
Click Run to execute your code

The Borrowing Rules

Rust enforces these rules at compile time to prevent data races:

References and Borrowing Rules
  1. You can have any number of immutable references to a value
  2. You can have only ONE mutable reference to a value
  3. You cannot have mutable and immutable references at the same time

Why These Rules?

These rules prevent data races, which occur when:

  • Two or more pointers access the same data at the same time
  • At least one pointer is being used to write to the data
  • There's no mechanism to synchronize access to the data
Output
Click Run to execute your code

Reference Scope and Lifetimes

A reference's scope starts where it's introduced and continues through its last use:

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s;  // OK! No immutable references are active
println!("{}", r3);
Non-Lexical Lifetimes (NLL): The Rust compiler is smart enough to see that r1 and r2 are not used after the println!, so it's safe to create a mutable reference.

Dangling References

In languages with pointers, it's easy to create a dangling pointer - a pointer that references a location in memory that may have been given to someone else. Rust guarantees that references will never be dangling:

Rust Prevents Dangling References
// This won't compile!
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s  // Error! s will be dropped, creating a dangling reference
// }

// Correct version - return the String itself
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // Ownership is moved out
}
Compile-Time Safety: The Rust compiler prevents dangling references at compile time. You'll get a helpful error message explaining the problem.

References vs Ownership

Aspect Ownership Immutable Reference Mutable Reference
Syntax T &T &mut T
Owns data Yes No No
Can modify Yes (if mut) No Yes
Multiple allowed No Yes No
Drops data Yes No No

Common Borrowing Mistakes

1. Multiple mutable references

Wrong:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;  // Error! Cannot have two mutable references
println!("{}, {}", r1, r2);

Fix - Use scopes:

let mut s = String::from("hello");
{
    let r1 = &mut s;
}  // r1 goes out of scope
let r2 = &mut s;  // OK!

2. Mixing mutable and immutable references

Wrong:

let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;  // Error! Cannot borrow as mutable
println!("{}, {}", r1, r2);

Fix - Separate usage:

let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);  // r1 is no longer used after this

let r2 = &mut s;  // OK!
println!("{}", r2);

3. Modifying through immutable reference

Wrong:

fn change(s: &String) {
    s.push_str("!");  // Error! Cannot mutate through & reference
}

Fix - Use mutable reference:

fn change(s: &mut String) {
    s.push_str("!");  // OK!
}

Borrowing Best Practices

  • Prefer borrowing over ownership: Use references when you don't need to own the data
  • Use immutable references by default: Only use &mut when you need to modify
  • Keep mutable reference scopes small: Limit the scope of mutable references
  • Let references expire: Use references and let them go out of scope before creating new ones
  • Trust the compiler: Borrowing errors are caught at compile time with helpful messages
  • Think about data flow: Design your functions to minimize mutable state

Exercise: Borrowing Practice

Task: Fix the borrowing errors in the code.

Requirements:

  • Fix immutable reference errors
  • Fix multiple mutable reference errors
  • Fix mixed reference errors
  • Use proper reference types
Output
Click Run to execute your code
Show Solution
fn main() {
    // Fix 1: Use mutable reference
    let mut s = String::from("hello");
    change_string(&mut s);
    println!("{}", s);
    
    // Fix 2: Separate mutable references
    let mut s1 = String::from("world");
    let r1 = &mut s1;
    r1.push_str("!");
    println!("{}", r1);
    // r1 is done, now create r2
    let r2 = &mut s1;
    r2.push_str("!");
    println!("{}", r2);
    
    // Fix 3: Separate immutable and mutable
    let mut s2 = String::from("Rust");
    let r1 = &s2;
    println!("{}", r1);
    // r1 is done
    let r2 = &mut s2;
    r2.push_str("!");
    println!("{}", r2);
    
    // Fix 4: Use reference
    let s3 = String::from("programming");
    let len = calculate_length_fixed(&s3);
    println!("Length of '{}' is {}", s3, len);
    
    // Fix 5: Use mutable reference
    let mut s4 = String::from("hello");
    append_world(&mut s4);
    println!("{}", s4);
}

fn change_string(s: &mut String) {
    s.push_str(", world");
}

fn calculate_length_fixed(s: &String) -> usize {
    s.len()
}

fn append_world(s: &mut String) {
    s.push_str(" world");
}

Summary

  • References let you refer to values without taking ownership
  • Borrowing is creating a reference
  • Immutable references (&T): Read-only access, multiple allowed
  • Mutable references (&mut T): Read-write access, only one at a time
  • Three borrowing rules: Multiple immutable OR one mutable, never both
  • References must always be valid - no dangling references
  • Reference scope ends at last use, not end of block
  • Borrowing rules prevent data races at compile time
  • The compiler provides helpful error messages for borrowing issues
  • References are zero-cost abstractions - no runtime overhead!

What's Next?

You now understand ownership and borrowing - the core of Rust's memory safety! In the next lesson, you'll learn about slices - a special kind of reference that lets you reference a contiguous sequence of elements in a collection rather than the whole collection.