Ownership: Rust's Superpower

Ownership is Rust's most unique feature. It enables memory safety without a garbage collector. Understanding ownership is crucial to mastering Rust.

For Python developers: This is the biggest mental shift. Python uses reference counting and garbage collection - Rust uses ownership. Take your time with this chapter!

The Problem Ownership Solves

Memory Management Approaches

ApproachLanguagesProsCons
ManualC, C++Fast, precise controlMemory leaks, dangling pointers
Garbage CollectionJava, Python, GoEasy, safeRuntime overhead, unpredictable pauses
OwnershipRustSafe, no runtime overheadLearning curve, compiler strictness

Python's approach:

# Python: GC manages memory automatically
s = "hello"
t = s  # Both reference same object
# GC cleans up when no references remain

Rust's approach:

// Rust: Ownership system manages memory at compile time
let s = String::from("hello");
let t = s;  // s moved to t, s no longer valid
// Memory freed when t goes out of scope (no GC!)

The Ownership Rules

Three simple rules that Rust enforces at compile time:

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

Variable Scope

fn main() {
    {                       // s is not valid here, not declared yet
        let s = "hello";    // s is valid from this point forward
        println!("{}", s);  // s can be used here
    }                       // scope ends, s is no longer valid
    
    // println!("{}", s);  // ERROR: s not in scope
}

The Stack vs The Heap

Stack

  • Fixed size data
  • Fast allocation/deallocation
  • Last-in, first-out (LIFO)
  • Examples: integers, floats, booleans, tuples, arrays
fn main() {
    let x = 5;           // Stored on stack
    let y = x;           // Copy x to y (both on stack)
    println!("{} {}", x, y);  // Both valid
}

Heap

  • Dynamic size data
  • Slower allocation/deallocation
  • Examples: String, Vec, HashMap
fn main() {
    let s1 = String::from("hello");  // Stored on heap
    let s2 = s1;                     // s1 moved to s2
    // println!("{}", s1);            // ERROR: s1 no longer valid
    println!("{}", s2);               // OK
}

Ownership and Move

Move Semantics

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

Python comparison:

# Python: Both variables reference same object
s1 = "hello"
s2 = s1  # s1 and s2 both valid, point to same string
print(s1)  # OK
print(s2)  # OK

What happens in Rust:

  1. s1 is created on the stack (pointer, length, capacity)
  2. The actual data ("hello") is on the heap
  3. s2 = s1 copies the stack data (pointer, not the heap data)
  4. s1 is invalidated to prevent double-free

This is called a move, not a shallow copy.

Copy vs Move

Copy Types (implement Copy trait):

  • All scalar types (integers, floats, bool, char)
  • Tuples containing only Copy types
  • Arrays of Copy types
fn main() {
    // Copy
    let x = 5;
    let y = x;
    println!("{} {}", x, y);  // Both valid
    
    // Move
    let s1 = String::from("hello");
    let s2 = s1;
    // println!("{}", s1);    // ERROR: moved
    println!("{}", s2);       // OK
}

Cloning

To actually duplicate heap data:

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

Warning: clone() can be expensive for large data.

Ownership and Functions

Passing Values

fn main() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s);  // ERROR: s was moved
    
    let x = 5;
    makes_copy(x);
    println!("{}", x);  // OK: x was copied
}

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

fn main() {
    let s1 = gives_ownership();
    println!("{}", s1);
    
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);
    // println!("{}", s2);  // ERROR: s2 was moved
    println!("{}", s3);     // OK
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string  // Returned and moved to the caller
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string  // Returned and moved to the caller
}

References and Borrowing

Problem: Passing ownership is tedious when you want to use a value later.

Solution: References allow you to refer to a value without taking ownership.

Immutable References

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // Borrow s1
    println!("The length of '{}' is {}.", s1, len);  // s1 still valid
}

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

Borrowing: Creating a reference (&) is called borrowing.

Mutable References

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);  // "hello, world"
}

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

Borrowing Rules

The Golden Rules:

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

Multiple Immutable References: OK

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("{}, {}, {}", r1, r2, r3);  // OK
}

Mutable and Immutable: ERROR

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;        // OK
    let r2 = &s;        // OK
    let r3 = &mut s;    // ERROR: cannot borrow as mutable
    println!("{}, {}, {}", r1, r2, r3);
}

Why? Users of immutable references don't expect the value to change!

Multiple Mutable References: ERROR

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;    // ERROR: cannot borrow as mutable more than once
    println!("{}, {}", r1, r2);
}

Why? Prevents data races at compile time.

Non-Lexical Lifetimes (NLL)

The scope of a reference ends when it's last used:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);
    // r1 and r2 are no longer used after this point
    
    let r3 = &mut s;  // OK: r1 and r2 are out of scope
    println!("{}", r3);
}

Dangling References

Rust prevents dangling references at compile time:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {  // ERROR
    let s = String::from("hello");
    &s  // We return a reference to s, but s is dropped!
}  // s goes out of scope and is dropped

Solution: Return the String directly (transfer ownership):

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

The Slice Type

A slice is a reference 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"
    
    // Shorthand
    let hello = &s[..5];    // Same as [0..5]
    let world = &s[6..];    // From 6 to end
    let entire = &s[..];    // Entire string
    
    println!("{} {}", hello, world);
}

Type: String slice is &str

String Literals are Slices

let s: &str = "Hello, world!";

String literals are stored in the binary and are immutable.

Slice as Function Parameter

fn first_word(s: &str) -> &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);  // Works with String
    
    let my_string_literal = "hello world";
    let word = first_word(my_string_literal);  // Works with &str
    
    println!("{}", word);
}

Best Practice: Use &str as parameter type to accept both String and string literals.

Array Slices

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

Common Patterns

Pattern 1: Read Only Access

fn process_data(data: &Vec<i32>) {
    for item in data {
        println!("{}", item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    process_data(&numbers);
    println!("Numbers: {:?}", numbers);  // Still valid
}

Pattern 2: Mutable Access

fn double_values(data: &mut Vec<i32>) {
    for item in data.iter_mut() {
        *item *= 2;
    }
}

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    double_values(&mut numbers);
    println!("{:?}", numbers);  // [2, 4, 6, 8, 10]
}

Pattern 3: Taking Ownership

fn consume_data(data: Vec<i32>) {
    println!("Consumed: {:?}", data);
}  // data is dropped here

fn main() {
    let numbers = vec![1, 2, 3];
    consume_data(numbers);
    // numbers is no longer valid
}

Pattern 4: Returning Owned Data

fn create_data() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

fn main() {
    let numbers = create_data();
    println!("{:?}", numbers);
}

Ownership Cheatsheet

Moves

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

takes_ownership(s2);   // s2 moved into function
// s2 is invalid

Borrows (Immutable)

let s = String::from("hello");
let r1 = &s;           // Borrow
let r2 = &s;           // Another borrow (OK)
// s, r1, r2 all valid

read_only(&s);         // Borrow for function call
// s still valid

Borrows (Mutable)

let mut s = String::from("hello");
let r = &mut s;        // Mutable borrow
// Only r is valid, not s
r.push_str(" world");
// After r is done, s is valid again

Copies

let x = 5;
let y = x;             // Copy (not move)
// x and y both valid

takes_copy(x);         // Copy into function
// x still valid

Practice Exercises

Exercise 1: Understanding Moves

Predict which lines will compile:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1);  // Will this compile?
    println!("{}", s2);  // Will this compile?
}

Exercise 2: Fix the Borrowing

Fix this code:

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;
    println!("{}", r1);
}

Exercise 3: Implement a Function

Write a function that:

  • Takes a string slice
  • Returns the number of words
  • Doesn't take ownership
fn count_words(s: &str) -> usize {
    // Your code here
}

fn main() {
    let text = String::from("hello world rust");
    let count = count_words(&text);
    println!("Words: {}", count);
    println!("Original: {}", text);  // Should still work
}

Exercise 4: Largest Element

Write a function that finds the largest element in a slice without taking ownership:

fn largest(list: &[i32]) -> i32 {
    // Your code here
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    let result = largest(&numbers);
    println!("Largest: {}", result);
    println!("Numbers: {:?}", numbers);  // Should still work
}

Common Mistakes

1. Using After Move

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);  // ERROR

Fix: Use clone() or borrow instead.

2. Multiple Mutable Borrows

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;  // ERROR

Fix: Use only one mutable reference at a time.

3. Mixing Mutable and Immutable Borrows

let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;  // ERROR
println!("{}", r1);

Fix: Finish using immutable references before creating mutable ones.

4. Returning References to Local Variables

fn dangle() -> &String {
    let s = String::from("hello");
    &s  // ERROR: s is dropped
}

Fix: Return the owned value: return s;

Ownership in Practice

Why Ownership Matters

// No garbage collection needed!
fn process_large_file() {
    let contents = read_file("large.txt");
    // Do something with contents
}  // contents automatically freed here - no GC pause!

// No memory leaks!
fn create_data() -> Vec<i32> {
    let data = vec![1, 2, 3];
    data
}  // Ownership transferred, caller is responsible

// No data races!
fn safe_concurrent() {
    let mut data = vec![1, 2, 3];
    // Can't create multiple mutable references
    // Compiler prevents data races at compile time!
}

Key Takeaways

  • Ownership ensures memory safety without garbage collection
  • Move semantics prevent double-free and use-after-free
  • Borrowing allows using values without taking ownership
  • References can be immutable (&T) or mutable (&mut T)
  • You can have multiple immutable OR one mutable reference
  • Slices provide safe views into sequences
  • The compiler enforces these rules at compile time - zero runtime cost

Next Steps

Now that you understand ownership, the next chapter covers control flow: if statements, loops, and pattern matching - the tools you'll use to direct your programs.

Quick Reference

// Ownership transfer
let s1 = String::from("hello");
let s2 = s1;  // s1 moved to s2

// Borrowing (immutable)
let r = &s2;

// Borrowing (mutable)
let mut s3 = String::from("hello");
let r = &mut s3;

// Clone (deep copy)
let s4 = s3.clone();

// Slices
let slice = &s4[0..5];
let arr_slice = &[1, 2, 3][1..];