DaleSchool

The Basic Rules of Ownership

Intermediate20min

Learning Objectives

  • State the three ownership rules
  • Observe how a String value moves between variables
  • Understand the error that occurs when using a variable after a move
  • Know that Copy types (integers, bool, etc.) are copied instead of moved

Working Code

Start by running this code in the Rust Playground.

fn main() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {name}!");
    println!("{greeting}");
    println!("Name: {name}");
}

Works fine, right? Now change it slightly:

fn main() {
    let name = String::from("Alice");
    let name2 = name;           // assign name to name2
    println!("Name: {name}");   // error!
}
error[E0382]: borrow of moved value: `name`
 --> src/main.rs:4:24
  |
2 |     let name = String::from("Alice");
  |         ---- move occurs because `name` has type `String`
3 |     let name2 = name;
  |                 ---- value moved here
4 |     println!("Name: {name}");
  |                      ^^^^ value borrowed here after move

What it means: "The value of name was moved to name2, so name can no longer be used."

Fix: After a move, use the new owner (name2), or create a copy with clone().

Try It Yourself

  1. In the broken code, change {name} to {name2} in the println!. Does the error go away?
  2. Replace let name2 = name; with let name2 = name.clone();. Can you now use both name and name2?

"Why?" — The Three Rules of Ownership

Rust's ownership rules boil down to three statements:

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

Think about it with the "owner of an item" analogy:

  • Your laptop has one owner (rule 2)
  • If you give your laptop to a friend (move), your friend is now the owner. You can no longer use it
  • When the owner leaves the room (scope ends), the laptop is cleaned up (rule 3)

Move — Like Shipping a Package

When you assign heap data like a String to another variable, the value moves. It's like shipping a package — once you ship it, you no longer have it.

fn main() {
    let original = String::from("package contents");
    let moved = original;  // shipped!

    // original is now empty-handed
    // moved is the new owner
    println!("{moved}");
}

Remember the error in Lesson 06 when you passed a vector to a function and tried to use it again? You fixed it with clone(). Now you can see why that error happened.

fn print_fruits(fruits: Vec<&str>) {
    for f in fruits {
        println!("{f}");
    }
}

fn main() {
    let fruits = vec!["apple", "banana"];
    print_fruits(fruits);       // ownership of fruits moves to the function!
    // println!("{:?}", fruits); // error! already moved
}

Passing a value to a function is also a move. The owner of fruits changed from main to the print_fruits function parameter.

Copy Types — Values That Get Copied

Integers and bool behave differently.

fn main() {
    let x = 42;
    let y = x;     // copied! (not moved)
    println!("x = {x}, y = {y}");  // both usable!
}

No error! Why?

Small values stored on the stack are cheap to copy. Rust gives these types the Copy trait so they are copied instead of moved.

| Copy types (copied) | Non-Copy types (moved) | | ---------------------------- | -------------------------- | | i32, f64, bool, char | String, Vec, HashMap | | Stack-stored, fixed size | Heap data, variable size |

Learn More: Why Not Always Use clone()?

clone() copies the entire heap data. For a small string that's fine, but cloning a vector with tens of thousands of elements is expensive in both memory and time.

fn main() {
    // Cloning 10,000 numbers — expensive!
    let big_data = vec![0; 10_000];
    let copy = big_data.clone();
    println!("Original: {}, Copy: {}", big_data.len(), copy.len());
}

In the next lesson you'll learn borrowing, which lets you use values without copying them. It's far more efficient than clone()!

Learn More: Scope and drop

When the owner goes out of scope, Rust automatically cleans up the value. This is called a drop.

fn main() {
    {
        let s = String::from("temporary!");
        println!("{s}");
    } // <- s is dropped here. Memory freed automatically!

    // println!("{s}"); // error! s is already gone
}

In C you had to call free() manually. Rust does it when the scope ends. You can never forget!

Exercise 1. Fix the code below so it compiles. Do it without using clone().

fn main() {
    let message = String::from("hello");
    let message2 = message;
    println!("{message}");
}

Hint: After a move, the original variable is unusable. Which variable should you print?

Exercise 2. Why does the code below compile without errors? Explain.

fn main() {
    let a = 10;
    let b = a;
    println!("a = {a}, b = {b}");
}

Hint: What special property does i32 have?

Q1. Which is NOT one of the three ownership rules?

  • A) Every value has an owner
  • B) There can be only one owner at a time
  • C) When the owner goes out of scope, the value is dropped
  • D) All values are automatically copied

Q2. What is the result of this code?

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s2}");
}
  • A) Compile error
  • B) Prints "hello"
  • C) Prints an empty string
  • D) Runtime error

Q3. What happens when you assign an i32 value to another variable?

  • A) It is moved, and the original variable becomes unusable
  • B) It is copied, and both variables are usable
  • C) A reference is created
  • D) A compile error occurs