Borrowing in Depth

Building on what we learned about ownership, this lesson takes a deeper look at borrowing - one of Rust's most powerful features for writing safe, efficient code without copying data.

Why Borrowing Matters

Without borrowing, you'd need to pass ownership around constantly, which can be inconvenient:

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    // s1 is moved, can't use it anymore!
    println!("The length of '{}' is {}.", s2, len);
}

// Awkward: returns the string back along with the length
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

With borrowing, this becomes much cleaner:

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

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

Immutable References

By default, references are immutable - you can read but not modify:

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

    // Create immutable reference
    let r1 = &s;
    let r2 = &s;  // Multiple immutable refs are OK

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

    println!("Original: {}", s);  // s is still valid
}

Mutable References

To modify borrowed data, use &mut:

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

    change(&mut s);

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

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

The Borrowing Rules

Rust enforces these rules at compile time:

Rule 1: One Mutable OR Many Immutable

You can have either:

  • One mutable reference, OR
  • Any number of immutable references

But never both at the same time:

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

    let r1 = &s;     // OK - first immutable ref
    let r2 = &s;     // OK - second immutable ref
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used

    let r3 = &mut s; // OK - mutable ref (no immutable refs active)
    println!("{}", r3);
}

This prevents data races:

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

    let r1 = &s;
    // let r2 = &mut s; // ERROR! Can't have mutable while immutable exists

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

Rule 2: References Must Be Valid

References must always point to valid data (no dangling references):

// This won't compile!
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s  // ERROR: s is dropped, reference would be invalid
// }

// Instead, return the owned value:
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // Ownership is moved out
}

fn main() {
    let s = no_dangle();
    println!("{}", s);
}

Non-Lexical Lifetimes (NLL)

Modern Rust uses NLL - references are considered "active" only until their last use, not until the end of scope:

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

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // r1 and r2's last use is here ^^^

    // This works because r1 and r2 are no longer "live"
    let r3 = &mut s;
    println!("{}", r3);
}

Reborrowing

You can reborrow from a mutable reference:

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

    // Reborrow: create immutable ref from mutable ref
    let r2 = &*r1;  // or just: let r2 = &r1;
    println!("{}", r2);

    // r1 is still valid after r2 is done
    r1.push_str(" world");
    println!("{}", r1);
}

Borrowing in Structs

Structs can hold references, but need lifetime annotations:

// Simple case: owned data (no lifetimes needed)
struct User {
    name: String,
    age: u32,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 30,
    };
    println!("User: {}, Age: {}", user.name, user.age);
}

Borrowing Patterns

Pattern 1: Read-Only Access

fn print_info(data: &Vec<i32>) {
    for item in data {
        println!("{}", item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    print_info(&numbers);
    print_info(&numbers);  // Can borrow again
}

Pattern 2: Modify in Place

fn double_values(data: &mut Vec<i32>) {
    for item in data.iter_mut() {
        *item *= 2;
    }
}

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    double_values(&mut numbers);
    println!("{:?}", numbers);  // [2, 4, 6, 8, 10]
}

Pattern 3: Split Borrowing

You can borrow different parts of a struct simultaneously:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut point = Point { x: 0, y: 0 };

    let x_ref = &mut point.x;
    let y_ref = &mut point.y;  // OK! Different fields

    *x_ref = 10;
    *y_ref = 20;

    println!("Point: ({}, {})", point.x, point.y);
}

Common Borrowing Errors

Error: Borrowed Value Moved

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

    // let s2 = s;  // ERROR: can't move while borrowed

    println!("{}", r);  // r still in use
}

Error: Mutable Borrow While Immutable Exists

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];

    // v.push(4);  // ERROR: can't mutate while immutably borrowed

    println!("First: {}", first);
}

Practice Exercise

fn main() {
    let mut message = String::from("Hello");

    // Multiple immutable borrows
    let len = get_length(&message);
    let first_char = get_first_char(&message);
    println!("Length: {}, First char: {:?}", len, first_char);

    // Mutable borrow after immutable borrows are done
    append_exclamation(&mut message);
    println!("Final: {}", message);
}

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

fn get_first_char(s: &String) -> Option<char> {
    s.chars().next()
}

fn append_exclamation(s: &mut String) {
    s.push('!');
}

Key Takeaways

  • Borrowing lets you use data without taking ownership
  • &T creates an immutable reference (read-only)
  • &mut T creates a mutable reference (read-write)
  • You can have many &T OR one &mut T, never both
  • References must always point to valid data
  • NLL makes the borrow checker smarter about when refs are "live"
  • Understanding borrowing is essential for writing idiomatic Rust

Master borrowing and you'll write safe, efficient Rust code!

Borrowing in Depth | LearningRust.org