Rust - References & Borrowing

Overview

Estimated time: 45–60 minutes

Master Rust's borrowing system that allows you to use values without taking ownership. Learn about references, borrowing rules, mutable vs immutable references, and how the borrow checker ensures memory safety at compile time.

Learning Objectives

Prerequisites

What are References?

References allow you to refer to a value without taking ownership of it. Think of a reference as a pointer that's guaranteed to point to a valid value for the life of that reference.

Creating References

Use the & operator to create a reference:

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

fn calculate_length(s: &String) -> usize {  // s is a reference to a String
    s.len()
}  // Here, s goes out of scope, but it doesn't drop what it refers to

Expected Output:

The length of 'hello' is 5.

References vs Ownership

Compare this to the ownership version we saw earlier:

fn main() {
    let s1 = String::from("hello");
    
    // Without references (takes ownership)
    let len = calculate_length_ownership(s1);  // s1 is moved
    // println!("{}", s1);  // Error: s1 no longer valid
    
    let s2 = String::from("world");
    
    // With references (borrows)
    let len2 = calculate_length_reference(&s2);  // s2 is borrowed
    println!("s2 is still valid: {}", s2);  // s2 is still valid!
    
    println!("Lengths: {}, {}", len, len2);
}

fn calculate_length_ownership(s: String) -> usize {
    s.len()
}  // s is dropped here

fn calculate_length_reference(s: &String) -> usize {
    s.len()
}  // s goes out of scope, but nothing is dropped

Expected Output:

s2 is still valid: world
Lengths: 5, 5

The Borrowing Rules

Rust enforces these borrowing rules at compile time:

Rule 1: References Must Always Be Valid

References must always point to valid data:

fn main() {
    let reference_to_nothing = dangle();  // This won't compile!
}

fn dangle() -> &String {  // Error: missing lifetime specifier
    let s = String::from("hello");
    &s  // We return a reference to data that's about to be dropped
}  // s is dropped here, so the reference would be invalid

Correct version:

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

fn no_dangle() -> String {  // Return the String directly, not a reference
    let s = String::from("hello");
    s  // Move s out of the function
}

Expected Output:

hello

Rule 2: Either Multiple Immutable References OR One Mutable Reference

At any given time, you can have either:

fn main() {
    let mut s = String::from("hello");
    
    // Multiple immutable references are OK
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    
    // This would cause an error if uncommented:
    // let r3 = &mut s;  // Error: cannot borrow as mutable while immutable refs exist
    
    // After the last use of immutable references, we can create a mutable one
    let r4 = &mut s;
    r4.push_str(", world");
    println!("{}", r4);
}

Expected Output:

hello and hello
hello, world

Immutable References

Multiple Immutable References

You can have as many immutable references as you want:

fn main() {
    let s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    
    println!("r1: {}, r2: {}, r3: {}", r1, r2, r3);
    println!("Original: {}", s);  // Original is still accessible
}

Expected Output:

r1: hello, r2: hello, r3: hello
Original: hello

Passing Immutable References to Functions

fn main() {
    let name = String::from("Alice");
    let age = 30;
    
    print_person_info(&name, &age);
    
    // Both variables are still available
    println!("After function call: {} is {}", name, age);
}

fn print_person_info(name: &String, age: &i32) {
    println!("Person: {} (age {})", name, age);
}

Expected Output:

Person: Alice (age 30)
After function call: Alice is 30

Mutable References

Creating Mutable References

Use &mut to create a mutable reference:

fn main() {
    let mut s = String::from("hello");
    
    change(&mut s);
    
    println!("{}", s);
}

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

Expected Output:

hello, world

Only One Mutable Reference at a Time

You can only have one mutable reference to a value in a particular scope:

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
    
    r1.push_str(", world");
    println!("{}", r1);
    
    // After r1 is no longer used, we can create another mutable reference
    let r2 = &mut s;
    r2.push_str("!");
    println!("{}", r2);
}

Expected Output:

hello, world
hello, world!

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
    
    println!("{} and {}", r1, r2);  // Last use of immutable references
    
    // Now we can create a mutable reference
    let r3 = &mut s;  // This is OK because r1 and r2 are no longer used
    r3.push_str(", world");
    println!("{}", r3);
}

Expected Output:

hello and hello
hello, world

Reference Scope and Non-Lexical Lifetimes

References End When Last Used

In modern Rust, references are valid until their last use, not the end of the scope:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);  // r1 and r2 end here (last use)
    
    let r3 = &mut s;  // This is OK because r1 and r2 are no longer active
    r3.push_str(", world");
    println!("{}", r3);
    
    // r3 ends here (last use)
    
    // We can create new references now
    let r4 = &s;
    println!("Final: {}", r4);
}

Expected Output:

hello and hello
hello, world
Final: hello, world

Common Borrowing Patterns

Borrowing in Loops

fn main() {
    let words = vec![
        String::from("hello"),
        String::from("world"),
        String::from("rust"),
    ];
    
    // Borrow each string instead of moving it
    for word in &words {  // Iterate over references
        println!("Word: {}", word);
    }
    
    // words is still valid because we borrowed, not moved
    println!("Total words: {}", words.len());
    
    // We can iterate again
    for (i, word) in words.iter().enumerate() {
        println!("{}: {}", i, word);
    }
}

Expected Output:

Word: hello
Word: world
Word: rust
Total words: 3
0: hello
1: world
2: rust

Borrowing for Partial Updates

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    
    // Borrow mutably to modify elements
    for num in &mut numbers {
        *num *= 2;  // Dereference to modify the value
    }
    
    println!("Doubled: {:?}", numbers);
    
    // Selective borrowing
    let first = &numbers[0];
    let last = &numbers[numbers.len() - 1];
    
    println!("First: {}, Last: {}", first, last);
}

Expected Output:

Doubled: [2, 4, 6, 8, 10]
First: 2, Last: 10

Dereferencing

Manual Dereferencing

Use * to dereference and access the value:

fn main() {
    let x = 5;
    let y = &x;
    
    assert_eq!(5, x);
    assert_eq!(5, *y);  // Dereference y to get the value
    
    println!("x: {}, *y: {}", x, *y);
    
    // With mutable references
    let mut a = 10;
    let b = &mut a;
    
    *b += 5;  // Dereference and modify
    println!("a: {}", a);
}

Expected Output:

x: 5, *y: 5
a: 15

Automatic Dereferencing

Rust automatically dereferences in many contexts:

fn main() {
    let s = String::from("hello world");
    let s_ref = &s;
    
    // These are equivalent
    println!("Length 1: {}", s.len());
    println!("Length 2: {}", s_ref.len());        // Auto-dereference
    println!("Length 3: {}", (*s_ref).len());     // Manual dereference
    
    // Method calls auto-dereference
    let starts_with_hello = s_ref.starts_with("hello");
    println!("Starts with hello: {}", starts_with_hello);
}

Expected Output:

Length 1: 11
Length 2: 11
Length 3: 11
Starts with hello: true

Borrowing with Structs

Borrowing Struct Fields

struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn get_name(&self) -> &String {  // Return a reference to the name
        &self.name
    }
    
    fn get_age(&self) -> u32 {  // Copy the age (u32 is Copy)
        self.age
    }
    
    fn celebrate_birthday(&mut self) {  // Mutable reference to self
        self.age += 1;
    }
}

fn main() {
    let mut person = Person {
        name: String::from("Alice"),
        age: 30,
    };
    
    // Immutable borrows
    let name_ref = person.get_name();
    let age_copy = person.get_age();
    
    println!("Name: {}, Age: {}", name_ref, age_copy);
    
    // Mutable borrow
    person.celebrate_birthday();
    
    println!("After birthday: {} is now {}", person.name, person.age);
}

Expected Output:

Name: Alice, Age: 30
After birthday: Alice is now 31

Partial Borrowing

You can borrow different fields of a struct simultaneously:

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

fn main() {
    let mut point = Point { x: 1, y: 2 };
    
    // Borrow different fields separately
    let x_ref = &mut point.x;
    let y_ref = &point.y;  // Immutable borrow of different field is OK
    
    *x_ref += *y_ref;
    
    println!("Point: ({}, {})", point.x, point.y);
}

Expected Output:

Point: (3, 2)

Advanced Borrowing Scenarios

Borrowing in Function Return Types

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let string2 = "xyz";
    
    let result = longest(&string1, string2);
    println!("The longest string is: {}", result);
}

Expected Output:

The longest string is: long string is long

Interior Mutability Pattern

Sometimes you need to mutate data through immutable references:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    // Even though `data` is not mutable, we can modify its contents
    *data.borrow_mut() += 1;
    
    println!("Value: {}", data.borrow());
    
    // Multiple immutable borrows are allowed
    let borrow1 = data.borrow();
    let borrow2 = data.borrow();
    
    println!("Borrow 1: {}, Borrow 2: {}", *borrow1, *borrow2);
}

Expected Output:

Value: 6
Borrow 1: 6, Borrow 2: 6

Common Borrowing Mistakes

Borrowing After Move

fn main() {
    let s = String::from("hello");
    let s_moved = s;  // s is moved to s_moved
    
    // let s_ref = &s;  // Error: s no longer valid
    let s_ref = &s_moved;  // This works
    
    println!("Reference: {}", s_ref);
}

Dangling References

fn main() {
    let reference;
    {
        let value = String::from("hello");
        // reference = &value;  // Error: value doesn't live long enough
    }
    // println!("{}", reference);  // reference would be dangling
    
    // Correct approach: move the value out
    let value = {
        String::from("hello")
    };
    let reference = &value;
    println!("{}", reference);
}

Expected Output:

hello

Attempting Multiple Mutable Borrows

fn main() {
    let mut vec = vec![1, 2, 3];
    
    // This would be an error:
    // let first = &mut vec[0];
    // let second = &mut vec[1];  // Error: multiple mutable borrows
    
    // Correct approach using split_at_mut
    let (left, right) = vec.split_at_mut(1);
    let first = &mut left[0];
    let second = &mut right[0];
    
    *first += 10;
    *second += 20;
    
    println!("Vector: {:?}", vec);
}

Expected Output:

Vector: [11, 22, 3]

Best Practices

Prefer Borrowing Over Ownership

// Good: accepts references
fn process_data(data: &[i32]) -> i32 {
    data.iter().sum()
}

// Less flexible: takes ownership
fn process_data_owned(data: Vec) -> i32 {
    data.iter().sum()
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Can call multiple times with references
    let sum1 = process_data(&numbers);
    let sum2 = process_data(&numbers);
    
    // Can still use numbers
    println!("Numbers: {:?}, Sum: {}, Sum again: {}", numbers, sum1, sum2);
}

Expected Output:

Numbers: [1, 2, 3, 4, 5], Sum: 15, Sum again: 15

Use Appropriate Reference Types

Performance Implications

Zero-Cost Abstractions

References are zero-cost at runtime - they compile to simple pointer operations:

fn process_large_data(data: &[u8; 1000000]) -> u8 {
    // No copying occurs - just passes a pointer
    data[0]
}

fn main() {
    let large_array = [42u8; 1000000];
    
    // This is as fast as passing a pointer in C
    let first_byte = process_large_data(&large_array);
    
    println!("First byte: {}", first_byte);
}

Expected Output:

First byte: 42

Common Pitfalls

Checks for Understanding

  1. What's the difference between &T and &mut T?
  2. Can you have multiple mutable references to the same data simultaneously?
  3. What happens when you try to use a reference after the value it points to is dropped?
  4. When does Rust automatically dereference references?
  5. What's the benefit of using references instead of moving values?

Answers

  1. &T is an immutable reference (read-only), &mut T is a mutable reference (read-write)
  2. No, you can only have one mutable reference at a time in a given scope
  3. Compile error - Rust prevents dangling references at compile time
  4. In method calls and when the context is clear (like comparisons)
  5. References allow multiple uses of data without transferring ownership, and they're zero-cost at runtime

← PreviousNext →