Developer Guides

Rust Ownership Explained: Borrowing and References

Discover Rust's unique memory management model, ownership, borrowing, and references explained with examples.

4 min read

Discover the fundamental memory management concepts in Rust — ownership, borrowing, and references — that allow Rust to guarantee memory safety without using a garbage collector. Understanding these principles will help you interpret common compiler errors and write more efficient, error-free code.

Understanding Ownership in Rust

Ownership is one of Rust's most defining features. It enforces memory safety by ensuring that every value in Rust has exactly one owner. This contrasts with languages that employ garbage collection (e.g., Java) or manual memory management (e.g., C++).

Benefits of Ownership

  • Memory Safety: No dangling pointers or use-after-free errors.
  • No Runtime Overhead: Rust’s memory checks occur at compile time, avoiding the unpredictability of garbage collection cycles.
  • Explicit Resource Management: Unlike manual memory allocation, Rust handles deallocations automatically when an owner goes out of scope.

Trade-offs

  • A steeper learning curve due to concepts like borrowing and lifetime annotations.
  • Potentially slower development as you adapt to ownership rules.

The Stack and Heap in Memory Management

To understand how Rust manages memory, it's crucial to differentiate between the stack and the heap.

  • Stack: Stores fixed-size data. Variables on the stack are pushed and popped in a Last In, First Out (LIFO) order. This makes access fast but limits dynamic or large data storage.
  • Heap: Allows dynamic memory allocation. Though versatile, it requires pointers, resulting in slower allocations and access compared to the stack.

For instance:

fn main() {
    let x = 5;               // Stored on the stack.
    let y = String::from("hello"); // String data is on the heap, but the pointer and its metadata live on the stack.
}

Rules of Ownership

Rust's memory model hinges on three key ownership rules:

Compiler Enforcement Example

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // Ownership moves to s2. s1 is no longer valid.
    println!("{}", s1);  // Error: `s1` is invalid after the move.
}

To retain both variables, use .clone():

let s2 = s1.clone();
println!("{}", s1); // Cloning duplicates the underlying data.

Understanding References and Borrowing

Borrowing allows you to access data without taking ownership. Rust achieves this using references, which can be immutable or mutable.

steps

  1. Declare an immutable reference:

    let s = String::from("hello");
    let len = calculate_length(&s);
    println!("Length: {}", len);
    
    fn calculate_length(s: &String) -> usize {
        s.len()
    }
    • The &s passed into the function borrows the value but doesn't claim ownership.
  2. Declare a mutable reference:

    fn main() {
        let mut s = String::from("hello");
        change(&mut s);
        println!("{}", s);
    }
    
    fn change(s: &mut String) {
        s.push_str(", world");
    }
    • You can only have one mutable reference or multiple immutable references at any given time to prevent data races.

Slices in Collections

Slices allow referencing a portion of a collection without owning it. They are most commonly used with strings or arrays.

String Slices

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..];

    println!("{} {}", hello, world);
}
  • &s[0..5] creates a slice referencing indices 0 to 4 (inclusive start, exclusive end).

Array Slices

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];
    println!("{:?}", slice); // Outputs `[2, 3]`
}

Slices are immutable, ensuring the underlying data remains safe during concurrent reads.

FAQ

Why does the Rust compiler say a value was "moved here"?

When you assign or pass a variable to another scope, Rust moves ownership rather than copying by default. This ensures no two owners modify the value simultaneously.

What are "references must be valid" errors in Rust?

Rust prevents references from pointing to invalid or dropped data. For example, returning a reference to a local variable leads to an error because the variable is deallocated when the function exits.

How does Rust prevent data races?

Rust enforces borrowing rules: you can have multiple immutable references or one mutable reference, but not both. This ensures there is no unsynchronized access to shared data.


Official reference: The Rust Programming Language.