1. rust
  2. /ownership
  3. /references-borrowing

References & Borrowing

References and borrowing allow you to use values without taking ownership of them. This is fundamental to writing efficient Rust code, as it enables you to access data without the expensive operations of moving or cloning values.

What are References?

A reference is like a pointer to data owned by another variable. Unlike pointers in other languages, Rust references are guaranteed to be valid and safe.

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 here because we didn't move it
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but because it's a reference, nothing is dropped

The & symbol creates a reference, and the process of creating a reference is called borrowing.

Immutable References

By default, references are immutable:

fn main() {
    let s = String::from("hello");
    let r1 = &s; // Immutable reference
    let r2 = &s; // Another immutable reference
    
    println!("{} and {}", r1, r2);
    // Multiple immutable references are allowed
    
    // This would not compile:
    // *r1 = String::from("world"); // Error: cannot assign
}

Multiple Immutable References

You can have multiple immutable references to the same data:

fn main() {
    let s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    
    println!("{}, {}, and {}", r1, r2, r3);
    // This is perfectly fine
}

Mutable References

To modify data through a reference, you need a mutable reference:

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

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

Mutable Reference Restrictions

You can have only one mutable reference to a particular piece of data at a time:

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);
}

This restriction prevents data races at compile time.

The Borrowing Rules

Rust enforces these borrowing rules at compile time:

  1. At any given time, you can have either:
    • One mutable reference, OR
    • Any number of immutable references
  2. References must always be valid

Rule 1: Exclusive Mutable Access

fn main() {
    let mut s = String::from("hello");
    
    // This works - one mutable reference
    {
        let r1 = &mut s;
        r1.push_str(", world");
    } // r1 goes out of scope here
    
    // This works - new scope, new reference
    let r2 = &mut s;
    println!("{}", r2);
}

Rule 1: No Mixing Mutable and Immutable

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 borrow as mutable
    
    println!("{} and {}", r1, r2);
    // After this point, r1 and r2 are no longer used
    
    let r3 = &mut s; // This is OK - immutable references are no longer active
    println!("{}", r3);
}

Reference Scope and Non-Lexical Lifetimes

References are valid from where they're created until their last use:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s; // Immutable reference starts
    let r2 = &s; // Another immutable reference starts
    
    println!("{} and {}", r1, r2);
    // r1 and r2 end here (last use)
    
    let r3 = &mut s; // Mutable reference starts after immutable ones end
    println!("{}", r3);
}

Dereferencing

Use the * operator to access the value a reference points to:

fn main() {
    let x = 5;
    let y = &x;
    
    assert_eq!(5, x);
    assert_eq!(5, *y); // Dereference y to get the value
    
    // This would not compile:
    // assert_eq!(5, y); // Error: can't compare integer with reference
}

Automatic Dereferencing

Rust automatically dereferences when calling methods:

fn main() {
    let s = String::from("hello world");
    let s_ref = &s;
    
    // These are equivalent:
    let len1 = s.len();
    let len2 = s_ref.len(); // Automatic dereferencing
    let len3 = (*s_ref).len(); // Manual dereferencing
    
    println!("Lengths: {}, {}, {}", len1, len2, len3);
}

Borrowing in Function Parameters

Taking References as Parameters

fn print_string(s: &String) {
    println!("{}", s);
} // s goes out of scope, but the String it points to is not dropped

fn modify_string(s: &mut String) {
    s.push_str(" modified");
}

fn main() {
    let mut original = String::from("hello");
    
    print_string(&original);
    modify_string(&mut original);
    
    println!("{}", original); // "hello modified"
}

Returning References

Functions can return references, but they must live long enough:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    &s[..]
}

fn main() {
    let my_string = String::from("hello world");
    let word = first_word(&my_string);
    println!("First word: {}", word);
}

Slices: A Special Kind of Reference

Slices are references to a contiguous sequence of elements:

String Slices

fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5];   // "hello"
    let world = &s[6..11];  // "world"
    let hello_alt = &s[..5]; // Same as &s[0..5]
    let world_alt = &s[6..]; // Same as &s[6..11]
    let whole = &s[..];     // Same as &s[0..11]
    
    println!("{} {}", hello, world);
}

Array Slices

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3]; // [2, 3]
    
    assert_eq!(slice, &[2, 3]);
    
    for element in slice {
        println!("{}", element);
    }
}

Common Borrowing Patterns

Borrowing for Reading

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

fn contains_char(s: &String, c: char) -> bool {
    s.contains(c)
}

fn main() {
    let text = String::from("hello world");
    
    println!("Length: {}", get_length(&text));
    println!("Contains 'o': {}", contains_char(&text, 'o'));
    
    // text is still available
    println!("Original: {}", text);
}

Borrowing for Modification

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

fn make_uppercase(s: &mut String) {
    *s = s.to_uppercase();
}

fn main() {
    let mut greeting = String::from("hello");
    
    append_exclamation(&mut greeting);
    println!("{}", greeting); // "hello!"
    
    make_uppercase(&mut greeting);
    println!("{}", greeting); // "HELLO!"
}

Borrowing in Loops

fn main() {
    let words = vec!["hello", "world", "rust"];
    
    // Borrowing each element
    for word in &words {
        println!("{}", word);
    }
    
    // words is still available
    println!("Original vector: {:?}", words);
    
    let mut numbers = vec![1, 2, 3, 4, 5];
    
    // Borrowing mutable references
    for num in &mut numbers {
        *num *= 2;
    }
    
    println!("Doubled: {:?}", numbers);
}

Advanced Borrowing Concepts

Partial Borrows

You can borrow different parts of a struct simultaneously:

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

fn main() {
    let mut point = Point { x: 5, y: 10 };
    
    let x_ref = &mut point.x;
    let y_ref = &point.y; // OK: borrowing different fields
    
    *x_ref += 1;
    println!("x: {}, y: {}", x_ref, y_ref);
}

Method Borrowing

impl Point {
    fn distance_from_origin(&self) -> f64 {
        ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }
    
    fn translate(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    let mut p = Point { x: 3, y: 4 };
    
    println!("Distance: {}", p.distance_from_origin()); // Immutable borrow
    p.translate(1, 1); // Mutable borrow
    println!("New position: ({}, {})", p.x, p.y);
}

Common Borrowing Mistakes and Solutions

Mistake 1: Trying to Modify Through Immutable Reference

fn main() {
    let mut s = String::from("hello");
    let r = &s; // Immutable reference
    
    // s.push_str(", world"); // Error: cannot borrow as mutable
    // *r = String::from("world"); // Error: cannot assign
    
    println!("{}", r);
    
    // Solution: Ensure reference is no longer needed
    // After r is last used, we can mutate s again
    s.push_str(", world");
    println!("{}", s);
}

Mistake 2: Multiple Mutable References

fn main() {
    let mut s = String::from("hello");
    
    // let r1 = &mut s;
    // let r2 = &mut s; // Error: multiple mutable borrows
    
    // Solution: Use scopes to separate borrows
    {
        let r1 = &mut s;
        r1.push_str(", world");
    }
    
    {
        let r2 = &mut s;
        r2.push('!');
    }
    
    println!("{}", s);
}

Mistake 3: Dangling References

// This would not compile:
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s // Error: returns a reference to data owned by function
// } // s goes out of scope and is dropped

// Solution: Return owned data instead
fn no_dangle() -> String {
    let s = String::from("hello");
    s // Ownership is moved out
}

Performance Benefits of Borrowing

Avoiding Unnecessary Clones

fn print_twice_bad(s: String) {
    println!("{}", s);
    println!("{}", s);
    // s is moved into this function, can't be used by caller
}

fn print_twice_good(s: &String) {
    println!("{}", s);
    println!("{}", s);
    // s is borrowed, caller retains ownership
}

fn main() {
    let my_string = String::from("hello");
    
    print_twice_good(&my_string);
    // my_string is still available
    println!("Still have: {}", my_string);
}

Efficient Data Processing

fn process_large_data(data: &[i32]) -> i32 {
    data.iter().sum() // Process without taking ownership
}

fn main() {
    let large_dataset = vec![1; 1_000_000];
    
    let sum = process_large_data(&large_dataset); // No expensive move/copy
    println!("Sum: {}", sum);
    
    // large_dataset is still available for other operations
}

Best Practices

1. Prefer Borrowing Over Ownership

// Good: Takes a reference
fn analyze_text(text: &str) -> (usize, usize) {
    (text.len(), text.chars().count())
}

// Less ideal: Takes ownership
// fn analyze_text(text: String) -> (usize, usize) {
//     (text.len(), text.chars().count())
// }

2. Use String Slices for Parameters

// Good: Accepts both &String and &str
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

// Less flexible: Only accepts &String
// fn greet(name: &String) {
//     println!("Hello, {}!", name);
// }

fn main() {
    let owned_string = String::from("Alice");
    let string_literal = "Bob";
    
    greet(&owned_string); // Works
    greet(string_literal); // Also works
}

3. Return Owned Data When Appropriate

fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name) // Return owned String
}

fn main() {
    let name = "World";
    let greeting = create_greeting(name);
    println!("{}", greeting);
}

References and borrowing are fundamental to Rust's memory safety guarantees. They allow you to write efficient code without sacrificing safety, enabling multiple readers or a single writer pattern that prevents data races at compile time.