Web Analytics

Ownership in Rust

Intermediate ~30 min read

Ownership is Rust's most unique feature. It enables Rust to make memory safety guarantees without needing a garbage collector. Understanding ownership is essential to writing Rust programs. In this lesson, you'll learn the three ownership rules, how memory works, and why ownership makes Rust special.

What is Ownership?

Ownership is a set of rules that govern how Rust manages memory. All programs must manage the way they use a computer's memory while running. Some languages have garbage collection, others require manual memory management. Rust uses a third approach: memory is managed through a system of ownership with rules that the compiler checks at compile time.

Real-World Analogy: Think of ownership like a library book system. When you check out a book (value), you become its owner. Only one person can own the book at a time. When you return it (variable goes out of scope), the library can lend it to someone else or remove it from the collection.

The Three Rules of Ownership

Keep these rules in mind as we work through examples:

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.
Output
Click Run to execute your code

The Stack and the Heap

To understand ownership, we need to understand how memory works. Both the stack and the heap are parts of memory available to your code at runtime, but they're structured differently.

Stack vs Heap Memory

The Stack

The stack stores values in the order it gets them and removes them in the opposite order (LIFO - Last In, First Out). All data stored on the stack must have a known, fixed size at compile time.

  • Fast: Adding and removing data is very quick
  • Fixed size: Size must be known at compile time
  • Automatic: Memory is automatically managed
  • Examples: Integers, booleans, chars, fixed-size arrays

The Heap

The heap is less organized. When you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot big enough, marks it as in use, and returns a pointer.

  • Slower: Allocating requires finding space
  • Dynamic size: Can grow or shrink at runtime
  • Manual management: Must be explicitly freed (Rust does this via ownership)
  • Examples: String, Vec, Box, HashMap
Performance Tip: Accessing data on the stack is faster than the heap because you don't have to follow a pointer. Modern processors are faster if they jump around less in memory.
Output
Click Run to execute your code

Move Semantics

When you assign a heap-allocated value to another variable, Rust moves the ownership rather than copying the data. This is different from many other languages!

Move Semantics Visualization
let s1 = String::from("hello");
let s2 = s1;  // s1 is moved to s2

// println!("{}", s1);  // Error! s1 is no longer valid
println!("{}", s2);  // OK! s2 is the owner
Why Move? If Rust allowed both s1 and s2 to be valid, when they both go out of scope, they would both try to free the same memory. This is called a double free error and is a memory safety bug. Rust prevents this by invalidating s1.

The Copy Trait

Some types implement the Copy trait. When a type implements Copy, variables are copied instead of moved. This applies to simple scalar values stored entirely on the stack.

Copy Types Move Types
All integers (i32, u64, etc.) String
Floating point (f32, f64) Vec<T>
bool Box<T>
char HashMap<K, V>
Tuples (if all elements are Copy) Any type with heap data
// Copy types - both variables are valid
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y);  // Both work!

// Move types - only one owner
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1);  // Error! s1 was moved

Deep Copying with Clone

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

let s1 = String::from("hello");
let s2 = s1.clone();  // Deep copy of heap data

println!("s1: {}, s2: {}", s1, s2);  // Both are valid!
Performance Note: Calling clone can be expensive because it copies all the heap data. Use it only when you actually need a deep copy.

Ownership and Functions

Passing a value to a function will move or copy, just like assignment:

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

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, nothing special happens

Returning Values and Ownership

Returning values can also transfer ownership:

Output
Click Run to execute your code

Common Ownership Mistakes

1. Using a value after it's been moved

Wrong:

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);  // Error: value borrowed after move

Fix Option 1 - Clone:

let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);  // OK! s1 still owns its data

Fix Option 2 - Use references (next lesson):

let s1 = String::from("hello");
let s2 = &s1;  // Borrow instead of move
println!("{}", s1);  // OK! s1 still owns its data

2. Forgetting that functions take ownership

Wrong:

let s = String::from("hello");
print_string(s);
println!("{}", s);  // Error: s was moved into function

fn print_string(text: String) {
    println!("{}", text);
}

Fix - Return ownership:

let s = String::from("hello");
let s = print_and_return(s);
println!("{}", s);  // OK!

fn print_and_return(text: String) -> String {
    println!("{}", text);
    text  // Return ownership
}

3. Moving in a loop

Wrong:

let words = vec![String::from("hello")];
for word in words {
    println!("{}", word);
}
println!("{:?}", words);  // Error: words was moved

Fix - Iterate by reference:

let words = vec![String::from("hello")];
for word in &words {  // Borrow each element
    println!("{}", word);
}
println!("{:?}", words);  // OK!

Ownership Best Practices

  • Prefer borrowing over moving: We'll learn about references in the next lesson
  • Use clone sparingly: It's expensive; only use when you truly need a deep copy
  • Return ownership when needed: Functions can return values to give ownership back
  • Understand Copy vs Move: Know which types implement Copy
  • Let the compiler guide you: Ownership errors are caught at compile time with helpful messages
  • Think about ownership early: Design your APIs with ownership in mind

Exercise: Ownership Practice

Task: Fix the ownership errors in the code.

Requirements:

  • Fix moved value errors
  • Use clone where appropriate
  • Return ownership from functions
  • Handle ownership in loops
Output
Click Run to execute your code
Show Solution
fn main() {
    // Fix 1: Clone s1
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1: {}, s2: {}", s1, s2);
    
    // Fix 2: Return ownership
    let s3 = String::from("world");
    print_string_fixed(s3.clone());
    println!("s3: {}", s3);
    
    // Fix 3: Return tuple
    let s4 = String::from("Rust");
    let (s4, len) = calculate_length_fixed(s4);
    println!("Length of '{}' is {}", s4, len);
    
    // Fix 4: Return both values
    let s5 = String::from("programming");
    let (s5, s6) = append_exclamation_fixed(s5);
    println!("s5: {}, s6: {}", s5, s6);
    
    // Fix 5: Clone for copy
    let x = String::from("copy");
    let y = x.clone();
    println!("x: {}, y: {}", x, y);
    
    // Fix 6: Use iter()
    let words = vec![
        String::from("hello"),
        String::from("world"),
    ];
    
    for word in &words {
        println!("{}", word);
    }
    
    println!("Words: {:?}", words);
}

fn print_string_fixed(s: String) {
    println!("{}", s);
}

fn calculate_length_fixed(s: String) -> (String, usize) {
    let len = s.len();
    (s, len)
}

fn append_exclamation_fixed(s: String) -> (String, String) {
    let mut result = s.clone();
    result.push('!');
    (s, result)
}

Summary

  • Ownership is Rust's most unique feature for memory safety
  • Three rules: Each value has one owner, only one owner at a time, value dropped when owner goes out of scope
  • Stack: Fast, fixed-size, automatic management
  • Heap: Slower, dynamic-size, ownership rules apply
  • Move semantics: Heap values are moved, not copied by default
  • Copy trait: Simple stack values are copied automatically
  • Clone: Explicit deep copy for heap data
  • Functions can take and return ownership
  • Ownership errors are caught at compile time
  • No garbage collector needed - zero runtime overhead!

What's Next?

Ownership is powerful, but taking and returning ownership with every function would be tedious. In the next lesson, you'll learn about references and borrowing - a way to use values without taking ownership. This makes Rust both safe and ergonomic.