Skip to editor content
learningrust.orglesson 5 of 26

Ownership & Borrowing

Ownership is Rust's most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector.

What is Ownership?

Ownership is a set of rules that govern how a Rust program manages memory. Unlike languages with garbage collection (like Java or Python) or manual memory management (like C), Rust uses a third approach: memory is managed through a system of ownership with rules that the compiler checks at compile time.

The Three Rules of Ownership

  1. Each value in Rust has a variable that's called its owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value will be dropped
fn main() {
    // s comes into scope
    let s = String::from("hello");

    // s is valid from this point forward
    println!("{}", s);

} // s goes out of scope and is dropped

The Stack and the Heap

To understand ownership, we need to understand how data is stored:

  • Stack: Fast, fixed-size data (integers, booleans, references)
  • Heap: Dynamic-size data (Strings, Vectors, etc.)
fn main() {
    // i32 is stored on the stack - copy is cheap
    let x = 5;
    let y = x; // y is a copy of x

    println!("x = {}, y = {}", x, y); // Both are valid!

    // String is stored on the heap
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2

    // println!("{}", s1); // ERROR: s1 is no longer valid
    println!("{}", s2); // Only s2 is valid
}

Move vs Copy

Types that have a known size at compile time implement the Copy trait:

fn main() {
    // These types implement Copy
    let a: i32 = 5;
    let b = a; // copy
    println!("a = {}, b = {}", a, b); // Both valid

    // Strings do NOT implement Copy
    let s1 = String::from("hello");
    let s2 = s1; // move, not copy
    // s1 is no longer valid here
}

Borrowing with References

Instead of taking ownership, you can borrow a value using references:

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

    // &s1 creates a reference to s1
    let len = calculate_length(&s1);

    // s1 is still valid because we only borrowed it!
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but it doesn't own the String

Mutable References

By default, references are immutable. To modify borrowed data, use mutable references:

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

    change(&mut s);

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

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

The Rules of References

  1. You can have either one mutable reference or any number of immutable references
  2. References must always be valid (no dangling references)
fn main() {
    let mut s = String::from("hello");

    // Multiple immutable references are OK
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // After r1 and r2 are no longer used, we can have a mutable reference
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r3);
}

Preventing Data Races

This restriction prevents data races at compile time:

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

    let r1 = &mut s;
    // let r2 = &mut s; // ERROR: cannot have two mutable references

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

The Slice Type

Slices let you reference a contiguous sequence of elements rather than the whole collection:

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

    let hello = &s[0..5];   // or &s[..5]
    let world = &s[6..11];  // or &s[6..]

    println!("{} {}", hello, world);
}

Practice Exercise

Try this code in the playground:

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

    // Borrow immutably to get length
    let length = get_length(&message);
    println!("Length: {}", length);

    // Borrow mutably to modify
    append_world(&mut message);
    println!("Message: {}", message);
}

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

fn append_world(s: &mut String) {
    s.push_str(", World!");
}

Key Takeaways

  • Ownership ensures memory safety without a garbage collector
  • Each value has exactly one owner
  • When the owner goes out of scope, the value is dropped
  • References allow borrowing without taking ownership
  • You can have one mutable reference OR multiple immutable references
  • The borrow checker enforces these rules at compile time

Understanding ownership is fundamental to writing safe, efficient Rust code!

Next Steps

Now that you understand ownership, you're ready to learn about structs and enums — Rust's tools for building custom data types that work hand in hand with the ownership system. You'll see how they let you model your domain precisely while the borrow checker keeps the data safe.

Next lesson

Structs & Enums

Learn how to define Rust structs and enums to create custom data types, implement methods, and model your domain effectively

20 min