4 min read
On this page

Ownership Rules

Ownership is the single concept that makes everything else in Rust click. If you understand ownership, you understand why the borrow checker exists, why lifetimes are needed, and why Rust can guarantee memory safety without a garbage collector. The rules are simple. Their implications are profound.

The Three Rules

  1. Every value in Rust has exactly one owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (freed).

That is it. Three rules. The entire memory safety model follows from these.

fn main() {
    {
        let s = String::from("hello"); // s owns the String
        println!("{}", s);
    } // s goes out of scope, String is dropped, memory is freed

    // s does not exist here
}
hello

No garbage collector scanned the heap. No reference counter was decremented. The compiler inserted a call to drop at the closing brace because it knows s is the owner and the owner is leaving scope.

Move Semantics

Assignment in Rust transfers ownership. This is called a move.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // ownership moves from s1 to s2

    // s1 is no longer valid
    // println!("{}", s1); // error: value used after move

    println!("{}", s2);
}
hello

Why does Rust do this? Consider what happens in C if two pointers point to the same heap allocation and both try to free it. Double-free is undefined behavior. By enforcing single ownership, Rust makes double-free impossible.

Moves happen in several contexts:

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

fn main() {
    let s = String::from("hello");

    take_ownership(s); // s is moved into the function

    // println!("{}", s); // error: value used after move

    let a = String::from("world");
    let b = a;           // moved by assignment
    let v = vec![b];     // moved into the vector
    // b is no longer valid
    println!("{:?}", v);
}
I own: hello
["world"]

Every assignment, every function call, every insertion into a collection — all of these can transfer ownership.

Clone for Explicit Copies

When you genuinely need two independent copies of heap data, use clone():

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // deep copy of the heap data

    println!("s1 = {}, s2 = {}", s1, s2); // both valid
}
s1 = hello, s2 = hello

clone() is explicit and visible. You can grep for it. You can measure its cost. There are no hidden copies in Rust — every allocation is intentional.

Stack vs Heap

Understanding the distinction clarifies why some types move and others copy.

Stack data has a known, fixed size at compile time. Integers, floats, booleans, fixed-size arrays, and tuples of stack types all live on the stack. Copying them is trivially cheap — just copy the bytes.

Heap data has a dynamic size. String, Vec<T>, HashMap<K, V>, and anything behind a Box<T> allocate on the heap. Copying them requires allocating new memory and copying potentially large amounts of data.

fn main() {
    // Stack types implement Copy — assignment copies, not moves
    let x: i32 = 42;
    let y = x;      // copied
    println!("x = {}, y = {}", x, y); // both valid

    // Heap types do not implement Copy — assignment moves
    let s1 = String::from("hello");
    let s2 = s1;    // moved
    // println!("{}", s1); // error
    println!("{}", s2);
}
x = 42, y = 42
hello

Types that implement the Copy trait are always copied on assignment. This includes:

  • All integer types (i8, i16, i32, i64, i128, u8, etc.)
  • Floating point types (f32, f64)
  • bool
  • char
  • Tuples containing only Copy types: (i32, f64) is Copy, (i32, String) is not
  • Fixed-size arrays of Copy types: [i32; 5] is Copy

Returning Ownership

Functions can give ownership back to the caller:

fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn add_exclamation(mut s: String) -> String {
    s.push('!');
    s // ownership transferred to caller
}

fn main() {
    let greeting = create_greeting("Rust");     // caller owns the result
    let excited = add_exclamation(greeting);     // greeting moved in, result moved out
    // greeting is no longer valid here
    println!("{}", excited);
}
Hello, Rust!!

This pattern — take ownership, transform, return ownership — is common in Rust. It makes the data flow explicit.

Ownership in Structs

Structs own their fields. When the struct is dropped, its fields are dropped.

struct User {
    name: String,
    email: String,
    age: u32,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
        age: 30,
    };

    println!("{} ({})", user.name, user.email);

    // Moving a field out of the struct:
    let name = user.name; // name field moved out
    // println!("{}", user.name); // error: field was moved
    println!("{}", user.email);   // other fields still valid
    println!("Moved name: {}", name);
}
Alice (alice@example.com)
alice@example.com
Moved name: Alice

Partial moves are allowed — you can move individual fields out of a struct, but then you cannot use the struct as a whole.

Drop Order

Values are dropped in reverse order of declaration. Struct fields are dropped in declaration order.

struct Noisy {
    name: String,
}

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("Dropping: {}", self.name);
    }
}

fn main() {
    let a = Noisy { name: String::from("first") };
    let b = Noisy { name: String::from("second") };
    let c = Noisy { name: String::from("third") };
    println!("All created");
}
All created
Dropping: third
Dropping: second
Dropping: first

Reverse order ensures that values created later (which might reference earlier values) are cleaned up first.

Real-World Example: File Handle Ownership

use std::fs::File;
use std::io::{self, Write};

fn write_report(path: &str) -> io::Result<()> {
    let mut file = File::create(path)?; // file owns the OS file handle
    writeln!(file, "Report generated")?;
    writeln!(file, "Status: OK")?;
    Ok(())
    // file is dropped here, which closes the OS file handle
}

fn main() {
    match write_report("/tmp/report.txt") {
        Ok(()) => println!("Report written"),
        Err(e) => eprintln!("Failed: {}", e),
    }
    // The file is guaranteed to be closed, even if an error occurred
}

No try/finally. No defer. No with statement. Ownership and Drop handle resource cleanup automatically and deterministically.

Common Pitfalls

  • Cloning to avoid move errors without understanding why. Every clone() is a heap allocation. Understand the ownership flow first, then decide if cloning is genuinely needed.
  • Not realizing that function arguments are moves. Passing a String to a function gives it away. If you need to keep using the value, pass a reference instead.
  • Confusing move with destruction. A move does not destroy the value — it transfers it. The value still exists, just under a different owner.
  • Fighting partial moves. If you move a field out of a struct, the struct is partially invalidated. Either clone the field, take a reference, or restructure your code.
  • Assuming Copy types are always small. [u8; 1000000] is Copy but copying a million bytes is not cheap. The compiler will copy it on assignment. Be aware of array sizes.

Key Takeaways

  • Every value has one owner. When the owner goes out of scope, the value is dropped.
  • Assignment moves ownership for heap types. Stack types that implement Copy are copied instead.
  • clone() creates explicit deep copies. There are no hidden copies in Rust.
  • Functions can take ownership of arguments and return ownership of results.
  • Ownership is the foundation of Rust's memory safety. Master it and the rest of the language follows.