1. What are References and Borrowing? #

References #

A reference is like a pointer - it’s an address that allows you to access data stored elsewhere without taking ownership of it. Unlike pointers in other languages, Rust references are guaranteed to point to valid values for their entire lifetime.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // &s1 creates a reference
    println!("The length of '{s1}' is {len}.");  // s1 is still valid here
}

fn calculate_length(s: &String) -> usize {  // s is a reference to a String
    s.len()
}  // s goes out of scope, but the String it points to is NOT dropped

Borrowing #

Borrowing is the act of creating a reference. Just like in real life, if someone owns something, you can borrow it, use it, and then give it back without taking ownership.

Key Insight: When functions take references as parameters, you don’t need to return the value to give back ownership because you never had ownership in the first place!

2. Types of References #

Immutable References (&T) #

By default, references are immutable - you cannot modify the data they point to.

fn main() {
    let s = String::from("hello");
    
    let r1 = &s;  // immutable reference
    println!("{}", r1);  // OK - reading is allowed
    
    // r1.push_str(", world");  // ERROR! Cannot modify through immutable reference
}

Mutable References (&mut T) #

Mutable references allow you to modify the borrowed data, but with strict rules.

fn main() {
    let mut s = String::from("hello");  // s must be mutable
    
    change(&mut s);  // pass mutable reference
    println!("{}", s);  // prints "hello, world"
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");  // OK - can modify through mutable reference
}

Reference Scope #

A reference’s scope starts when it’s introduced and ends at its last usage, not necessarily at the end of the code block.

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

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // r1 and r2 scope ends here (last usage)

    let r3 = &mut s;  // OK - no overlap with r1, r2
    println!("{}", r3);
}

3. Multiple Mutable References: The Strict Rules #

Cannot Have Multiple Mutable References Simultaneously #

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

    let r1 = &mut s;
    let r2 = &mut s;  // ERROR! Cannot borrow s as mutable more than once

    println!("{}, {}", r1, r2);
}

Why This Restriction Exists:

  • Prevents data races at compile time
  • A data race occurs when:
    • Two+ pointers access the same data simultaneously
    • At least one pointer is writing to the data
    • No synchronization mechanism exists

Can Have Multiple Mutable References in Different Scopes #

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

    {
        let r1 = &mut s;
        // use r1
    }  // r1 goes out of scope

    let r2 = &mut s;  // OK - not simultaneous
}

4. Multiple Immutable References: The Flexible Rules #

Can Have Multiple Immutable References #

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

    let r1 = &s;  // OK
    let r2 = &s;  // OK  
    let r3 = &s;  // OK - multiple immutable references allowed

    println!("{}, {}, {}", r1, r2, r3);
}

Why This is Safe:

  • Reading data doesn’t affect other readers
  • No one can modify the data through immutable references
  • No risk of data races

Cannot Mix Mutable and Immutable References #

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

    let r1 = &s;        // immutable reference
    let r2 = &s;        // another immutable reference  
    let r3 = &mut s;    // ERROR! Cannot have mutable reference while immutable ones exist

    println!("{}, {}, {}", r1, r2, r3);
}

Why This is Forbidden:

  • Users of immutable references expect the data to never change
  • A mutable reference could modify the data while immutable references exist
  • This would violate the “no surprise changes” guarantee

Sequential Usage is Fine (Non-Overlapping Scopes) #

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

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);  // last usage of r1 and r2
    // r1 and r2 scope ends here

    let r3 = &mut s;  // OK - no overlap with r1, r2
    println!("{}", r3);
}

5. Key Memory Safety Guarantees #

No Dangling References #

Rust prevents references to deallocated memory:

fn dangle() -> &String {  // ERROR! Missing lifetime specifier
    let s = String::from("hello");
    &s  // s will be dropped, reference would be invalid
}  

fn no_dangle() -> String {  // OK - return owned value
    let s = String::from("hello");
    s  // ownership moved out
}

6. The Golden Rules of References #

  1. At any time, you can have EITHER:

    • One mutable reference, OR
    • Any number of immutable references
  2. References must always be valid (no dangling references)

  3. Reference scopes end at last usage, not at closing braces

  4. Functions with reference parameters don’t need to return values to give back ownership

7. Common Patterns and Best Practices #

Function Parameters #

// Good: Take reference when you only need to read
fn get_length(s: &String) -> usize {
    s.len()
}

// Good: Take mutable reference when you need to modify
fn append_world(s: &mut String) {
    s.push_str(", world");
}

// Avoid: Taking ownership unless you need to consume the value
fn bad_length(s: String) -> usize {  // Takes ownership unnecessarily
    s.len()
}  // s is dropped here - wasteful!

Working with Scopes #

fn main() {
    let mut data = String::from("hello");
    
    // Pattern: Use immutable references for reading
    {
        let r1 = &data;
        let r2 = &data;
        println!("Reading: {} {}", r1, r2);
    }  // immutable references end
    
    // Pattern: Use mutable reference for modification
    {
        let r3 = &mut data;
        r3.push_str(", world");
        println!("Modified: {}", r3);
    }  // mutable reference ends
    
    println!("Final: {}", data);
}

Summary #

References and borrowing are Rust’s way of allowing safe access to data without transferring ownership. The strict rules around mutable and immutable references prevent data races and ensure memory safety at compile time. While these rules might seem restrictive initially, they eliminate entire classes of bugs that plague other systems programming languages.

The key insight is that:

  • Rust enforces these rules at compile time
  • Catches potential issues before your code ever runs
  • Leading to safer and more reliable programs