Ownership

Rust’s ownership system is the language’s defining feature. It replaces garbage collection with compile-time rules that guarantee memory safety and prevent data races.

The Three Rules

  1. Each value has exactly one owner (a variable)
  2. When the owner goes out of scope, the value is dropped (freed)
  3. There can only be one owner at a time
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;        // s1's ownership MOVES to s2

    // println!("{s1}");  // Error: s1 no longer valid
    println!("{s2}");     // Works fine
}

Move Semantics

Assignment transfers ownership for heap-allocated types. This is a move, not a copy.

fn take_ownership(s: String) {
    println!("{s}");
}   // s is dropped here, memory freed

fn main() {
    let name = String::from("Alice");
    take_ownership(name);
    // name is no longer valid here - it was moved into the function
}

Tip: Types that implement Copy (integers, booleans, floats, chars) are copied instead of moved. Stack-only data is cheap to duplicate.

let x = 42;
let y = x;      // x is copied, not moved
println!("{x}"); // both still valid
println!("{y}");

Borrowing with References

Instead of transferring ownership, you can borrow a value with references.

Shared references (&T) - read-only

fn print_length(s: &String) {
    println!("Length: {}", s.len());
}   // s goes out of scope, but it doesn't own the String, so nothing is dropped

fn main() {
    let name = String::from("Alice");
    print_length(&name);    // borrow name
    println!("{name}");      // still valid - we only lent it
}

Mutable references (&mut T) - read and write

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

fn main() {
    let mut name = String::from("Alice");
    add_greeting(&mut name);
    println!("{name}"); // "Alice, hello!"
}

The Borrowing Rules

These two rules prevent data races at compile time:

RuleWhat it means
Many &T OR one &mut TYou can have multiple readers or one writer, never both
References must be validA reference cannot outlive the data it points to
let mut data = vec![1, 2, 3];

let first = &data[0];      // shared borrow
// data.push(4);            // Error: can't mutate while shared borrow exists
println!("{first}");        // shared borrow used here

data.push(4);               // OK: shared borrow is no longer in use

Gotcha: The borrow checker tracks where references are last used, not where they go out of scope. This is called Non-Lexical Lifetimes (NLL) and is why the example above compiles.

Common Pattern: Take &str Not String

Functions that only need to read string data should accept &str. This works with both String and string literals.

// Bad: forces caller to give up ownership or clone
fn greet_bad(name: String) {
    println!("Hello, {name}");
}

// Good: borrows a string slice, works with &String and &str
fn greet(name: &str) {
    println!("Hello, {name}");
}

fn main() {
    let owned = String::from("Alice");
    greet(&owned);        // &String coerces to &str
    greet("Bob");         // &str literal works directly
    println!("{owned}");  // owned is still valid
}

Fighting the Borrow Checker

When the borrow checker rejects your code, the fix is usually one of these:

ProblemSolution
Need the value in two placesClone it (.clone()) or restructure to use references
Mutating while iteratingCollect indices first, then mutate. Or use .retain() / .iter_mut()
Self-referential structUse indices instead of references, or use Pin
Complex shared ownershipUse Rc<T> (single-thread) or Arc<T> (multi-thread)
// Clone when ownership semantics get complicated
let config = load_config();
let config_for_server = config.clone();
let config_for_logger = config.clone();

// Rc for shared ownership without cloning data
use std::rc::Rc;
let shared = Rc::new(expensive_data());
let ref1 = Rc::clone(&shared); // cheap reference count bump
let ref2 = Rc::clone(&shared);

Tip: Reaching for .clone() is perfectly fine when you’re learning. Optimize ownership patterns later once the code works. The compiler’s suggestions are usually good - read the error messages carefully.

Why This Matters

Ownership eliminates entire categories of bugs at compile time:

  • No double frees - only one owner drops the value
  • No dangling pointers - references can’t outlive data
  • No data races - mutable access is exclusive
  • No null pointer dereferences - Rust has no null (uses Option instead)
  • No use-after-free - moved values can’t be accessed

These guarantees hold without runtime overhead. No garbage collector, no reference counting (unless you opt into Rc/Arc).

Next: Traits | Error Handling