Ownership: One Rule, Three Corollaries

Ownership is one rule with three corollaries. Each value has one owner. When the owner goes out of scope, the value is dropped. Assigning a value moves it, and the old name is gone.

This is weird at first. You will write code that looks obviously correct and the compiler will refuse it. That is the compiler doing its job. Read the message, adjust, move on.

For Python developers: this is the biggest mental shift. Python uses reference counting and a garbage collector. Rust uses ownership. Take your time.

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 rules the compiler enforces:

  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

Passing ownership is tedious when you want to use a value later. References fix that. A reference lets you point at 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 two rules:

  1. At any moment, you have either one mutable reference, or any number of immutable references. Never both at once.
  2. References must always be valid. No dangling pointers.

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 the parameter type. It accepts both String (via deref) 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 gives memory safety without garbage collection
  • Move semantics prevent double-free and use-after-free
  • Borrowing lets you use values without taking ownership
  • References are immutable (&T) or mutable (&mut T)
  • You get many immutable, or exactly one mutable, never both
  • Slices give safe views into sequences
  • The compiler enforces these rules at compile time with zero runtime cost

Next Steps

Continue to 04-control-flow.md to learn if, loops, and pattern matching.

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..];