1. rust
  2. /ownership
  3. /understanding-ownership

Understanding Ownership

Ownership is Rust's most unique feature and the foundation of memory safety without garbage collection. Understanding ownership is crucial for writing effective Rust code and unlocks the language's guarantees around memory safety and concurrency.

What is Ownership?

Ownership is a set of rules that govern how Rust manages memory. Unlike languages with garbage collectors or manual memory management, Rust uses ownership to automatically manage memory at compile time, preventing memory leaks and use-after-free errors.

The Ownership Rules

Rust's ownership system is built on three fundamental rules:

  1. Each value in Rust has an owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value will be dropped

Let's explore each rule in detail.

Rule 1: Each Value Has an Owner

Every piece of data in Rust has exactly one variable that "owns" it:

fn main() {
    let s = String::from("hello"); // s owns the String
    // The String data is owned by s
}

The variable s is the owner of the String containing "hello".

Rule 2: Only One Owner at a Time

A value can have only one owner at any given time:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership moves from s1 to s2
    
    // println!("{}", s1); // Error! s1 no longer owns the data
    println!("{}", s2); // This works fine
}

When we assign s1 to s2, ownership of the String moves to s2. This is called a move.

Rule 3: Values are Dropped When Owner Goes Out of Scope

When a variable goes out of scope, Rust automatically calls the drop function to clean up the memory:

fn main() {
    {
        let s = String::from("hello"); // s comes into scope
        // Do stuff with s
    } // s goes out of scope and is dropped here
    
    // s is no longer valid here
}

The Stack vs The Heap

Understanding the difference between stack and heap is crucial for ownership:

Stack Data

Data with a known, fixed size at compile time is stored on the stack:

fn main() {
    let x = 5;        // i32 stored on stack
    let y = x;        // Copy the value, both x and y are valid
    
    println!("x: {}, y: {}", x, y); // Both work fine
}

Simple scalar types implement the Copy trait, so they're copied rather than moved.

Heap Data

Data with unknown size at compile time or that might change is stored on the heap:

fn main() {
    let s1 = String::from("hello"); // String data stored on heap
    let s2 = s1;                    // Ownership moves to s2
    
    // s1 is no longer valid
}

Copy vs Move Semantics

Rust has two ways to transfer values: copy and move.

Copy Types

Types that implement the Copy trait are copied when assigned:

fn main() {
    // These types implement Copy
    let x = 5;           // i32
    let y = true;        // bool
    let z = 'a';         // char
    let tuple = (1, 2);  // Tuple of Copy types
    
    let a = x; // x is copied, both x and a are valid
    println!("x: {}, a: {}", x, a);
}

Types that implement Copy:

  • All integer types (i32, u64, etc.)
  • Boolean type (bool)
  • Character type (char)
  • Floating point types (f32, f64)
  • Tuples containing only Copy types

Move Types

Types that don't implement Copy are moved when assigned:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is moved to s2
    
    // println!("{}", s1); // Error: value borrowed here after move
    println!("{}", s2);   // This works
}

Functions and Ownership

Passing values to functions follows the same ownership rules:

Moving into Functions

fn main() {
    let s = String::from("hello");
    takes_ownership(s); // s is moved into the function
    
    // s is no longer valid here
    // println!("{}", s); // Error!
    
    let x = 5;
    makes_copy(x); // x is copied (i32 implements Copy)
    
    println!("{}", x); // x is still valid
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string goes out of scope and is dropped

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} // some_integer goes out of scope, but nothing special happens

Returning Values and Ownership

fn main() {
    let s1 = gives_ownership(); // Function moves its return value into s1
    
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2 is moved into the function,
                                      // which also moves its return value into s3
    
    // s1 and s3 are valid here, s2 is not
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string // Return value is moved out
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string // Return value is moved out
}

The Problem with Move Semantics

Moving values can be inconvenient when you want to use a value after passing it to a function:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(s1); // s1 is moved
    
    // println!("{}", s1); // Error! s1 is no longer valid
    println!("Length: {}", len);
}

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

This is where references and borrowing come in, which we'll cover in the next section.

Clone for Deep Copying

If you want to deeply copy heap data, use the clone method:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // Deep copy the heap data
    
    println!("s1: {}, s2: {}", s1, s2); // Both are valid
}

Note: Cloning can be expensive as it copies all the heap data.

Ownership in Different Contexts

With Structs

struct User {
    username: String,
    email: String,
    active: bool,
}

fn main() {
    let user1 = User {
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        active: true,
    };
    
    // This moves the String fields
    let user2 = User {
        username: user1.username, // username moved from user1
        email: String::from("[email protected]"),
        active: user1.active, // bool implements Copy
    };
    
    // user1 is partially moved - can't use the whole struct
    // println!("{}", user1.username); // Error!
    println!("{}", user1.active); // This still works (Copy type)
}

With Collections

fn main() {
    let mut v = Vec::new();
    v.push(String::from("hello"));
    v.push(String::from("world"));
    
    let first = v[0]; // Error! Can't move out of index
    
    // Use indexing with references instead
    let first = &v[0]; // Borrow instead of move
    println!("{}", first);
    
    // Or use methods that take ownership
    let first = v.into_iter().next().unwrap(); // Consumes the vector
}

Ownership and Loops

Be careful with ownership in loops:

fn main() {
    let strings = vec![
        String::from("hello"),
        String::from("world"),
    ];
    
    // This moves each string out of the vector
    for s in strings { // strings is moved here
        println!("{}", s);
    }
    
    // println!("{:?}", strings); // Error! strings has been moved
    
    // To avoid moving, use references
    let strings = vec![
        String::from("hello"),
        String::from("world"),
    ];
    
    for s in &strings { // Borrow instead of move
        println!("{}", s);
    }
    
    println!("{:?}", strings); // strings is still valid
}

Box and Heap Allocation

Box<T> provides a way to store data on the heap with a known size:

fn main() {
    let b = Box::new(5); // Allocate an i32 on the heap
    println!("b = {}", b);
    
    let large_box = Box::new([0; 1_000_000]); // Large array on heap
    
    // Box owns the heap data and will clean it up when dropped
}

Ownership Patterns

The RAII Pattern

Resource Acquisition Is Initialization - resources are tied to object lifetime:

use std::fs::File;

fn main() {
    let file = File::open("example.txt").unwrap();
    // File is automatically closed when `file` goes out of scope
}

Transfer of Ownership Pattern

fn process_string(s: String) -> String {
    // Process the string
    format!("Processed: {}", s)
}

fn main() {
    let original = String::from("hello");
    let processed = process_string(original); // Transfer ownership
    // original is no longer valid, but processed is
}

Common Ownership Mistakes

Using Moved Values

fn main() {
    let s = String::from("hello");
    let s2 = s; // s is moved
    
    // println!("{}", s); // Error: borrow of moved value
}

Partial Moves in Structs

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

fn main() {
    let p = Point {
        x: String::from("1"),
        y: String::from("2"),
    };
    
    let x = p.x; // Partial move
    // println!("{}", p.y); // Error: cannot use p.y after partial move
}

Benefits of Ownership

  1. Memory Safety: No use-after-free, double-free, or memory leaks
  2. No Runtime Overhead: All checks happen at compile time
  3. Thread Safety: Ownership prevents data races
  4. Predictable Performance: No garbage collection pauses

Mental Model for Ownership

Think of ownership like having a unique key to a resource:

  • Only one person can hold the key at a time
  • When you give the key to someone else, you no longer have access
  • When the key holder leaves, the resource is automatically cleaned up
  • You can make copies of the key for some simple resources (Copy types)

Understanding ownership is fundamental to mastering Rust. It enables memory safety without garbage collection and forms the foundation for Rust's powerful concurrency features. In the next sections, we'll explore how borrowing and references provide flexibility within the ownership system.