Hello! Welcome back to your journey into Rust.
In our last lesson, you did a great job distinguishing between String and &str. We saw that a String is owned, growable data, while a &str is a borrowed, immutable view of that data. You might have been wondering why Rust makes this distinction. The answer lies in its most famous and powerful feature: the ownership model.
Today, we're going to dive into that model. You will learn what ownership is, how it enables Rust to guarantee memory safety without a garbage collector, and you'll understand the concept of move semantics—how Rust transfers ownership of data between variables. This is a fundamental concept that, once grasped, will make writing Rust feel much more intuitive. It's the key to working with the compiler, not against it.
1. Memory, Ownership, and The Three Rules
Coming from Java and C, you've seen two different approaches to memory management. In C, you manually allocate and free memory (malloc/free), which is fast but can lead to bugs like memory leaks or using freed memory. In Java, a garbage collector runs in the background, cleaning up unused objects, which is safer but can introduce performance overhead.
Rust takes a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. This means you get the performance of C-style manual management with the memory safety guarantees of a garbage-collected language, all without a runtime garbage collector.
To understand ownership, we first need a quick refresher on how a program uses memory. The following video provides a great analogy for the two main memory areas: the stack and the heap.
Learning Rust: Memory, Ownership and Borrowing
This video from the YouCodeThings channel introduces the concepts of the stack and the heap using a simple analogy. It then lays out the three fundamental rules of ownership that govern how Rust manages this memory.
Watch from the beginning to 03:33. Focus on: The 'desk and paper' analogy for the stack (00:34). The 'locker room' analogy for the heap (02:11). The three rules of ownership presented at the end (03:13).
As the video explained, the rules of ownership are simple but have profound implications:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped (i.e., the memory is freed).
These three rules are the bedrock of Rust's memory safety. Let's see how they work in practice.
2. Move Semantics: Transferring Ownership
Now for the main event: what happens when you assign a value from one variable to another? Based on the second rule—"There can only be one owner at a time"—we can't just have two variables owning the same piece of data.
Consider this code with the String type we learned about, which stores its text on the heap:
let s1 = String::from("hello");
let s2 = s1;
In many languages, this would create a copy of the string. In Rust, something different happens: ownership is moved.
After let s2 = s1;, s2 is now the owner of the string data. The second ownership rule dictates that s1 can no longer be the owner. To enforce this, the compiler considers s1 invalidated and will not let you use it again.
This visual diagram illustrates what's happening. The data on the heap isn't copied; only the pointer, length, and capacity on the stack are. Then, the original variable is invalidated.

If you try to use s1 after the move, the compiler will stop you with an error. This is one of the most common errors for new Rust programmers, and it directly prevents potential bugs like "double-freeing" memory.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership of the String data is moved from s1 to s2
// This next line will not compile! Try it yourself.
// println!("s1 is: {}", s1);
println!("s2 is: {}", s2); // This is fine, s2 is the owner.
}
This transfer of ownership is called a move.
3. Move vs. Copy: An Important Distinction
You might be thinking, "Wait, in the first lesson, I could do this with numbers and it worked fine!" You are absolutely right.
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // This works perfectly!
Why does this work, but the String example fails?
The reason is that some types, like integers, booleans, and floating-point numbers, have a special trait called Copy. For these types, which have a known, fixed size and live entirely on the stack, Rust makes a full copy of the data when you reassign them. It's cheap to do, so there's no need to invalidate the original variable.
Types that manage heap data, like String, do not implement the Copy trait. For them, Rust defaults to the safer and more performant move semantic.
The following reading explains this distinction clearly. It's written in an informal style that tackles the exact points of confusion you might be having right now.
Rust Mutability, Moving and Borrowing - The Straight Dope
This article, 'Rust Mutability, Moving and Borrowing - The Straight Dope', provides a fantastic, no-nonsense explanation of move semantics and the 'gotcha' with Copy types.
Read the sections titled 'What the hell is a "move?"' and 'BUT WAIT - YOU WILL PULL YOUR HAIR OUT OVER THIS!!!!'. They perfectly capture the experience of learning this concept.
4. Ownership and Functions
The same ownership rules apply when passing values to functions.
- Passing a
Copytype (likei32) to a function copies the value. - Passing an owned type (like
String) to a function moves ownership to the function's parameter. The original variable becomes invalid, just as if you had assigned it to another variable.
This video demonstrates this concept visually.
Learning Rust: Memory, Ownership and Borrowing
Let's return to the YouCodeThings video to see how ownership is transferred when a value is passed to a function.
Watch the section from 05:31 to 06:24. It walks through what happens on the stack when a function takes ownership of a string and the scope ends.
Here's a code example demonstrating this.
fn main() {
let my_string = String::from("This is my string");
// takes_ownership will take ownership of my_string
takes_ownership(my_string);
// This line would cause a compile error because my_string's ownership was moved
// println!("{}", my_string);
}
// This function takes ownership of the String passed to it.
fn takes_ownership(some_string: String) {
println!("I now own this string: '{}'", some_string);
} // Here, some_string goes out of scope and `drop` is called. The memory is freed.
Hands-On Activity: Fight the Compiler!
The best way to learn ownership is to see the compiler enforce it. Let's do a quick exercise in your rust_chess project.
- Open
src/main.rsand clear out themainfunction. - Create a
Stringvariable, for example,let piece = String::from("Pawn");. - Create a new variable and move the ownership, e.g.,
let moved_piece = piece;. - Now, try to print the original
piecevariable. - Run
cargo runand read the compiler error carefully. It will tell you exactly what happened ("use of moved value"). - Comment out the line that caused the error.
- Now, define a simple function outside
maincalledprint_piecethat takes one parameter of typeStringand prints it. - In
main, create anotherStringand callprint_piecewith it. - After the function call, try to print that
Stringagain inmain. - Run
cargo runagain. Observe the similar "use of moved value" error.
This exercise forces you to confront the exact scenarios where ownership moves cause compiler errors, helping you build an intuition for the rules.
Conclusion
Fantastic work! You've just tackled the single most unique and challenging concept in Rust. It might feel restrictive at first, but this system is what makes Rust so powerful and safe.
Key Takeaways:
- Rust uses an ownership model with three rules to manage memory safely without a garbage collector.
- Values that manage heap resources (like
String) are moved when assigned to a new variable or passed to a function. The original owner becomes invalid. - Simple values stored entirely on the stack (like
i32,bool) are copied. The original variable remains valid. - The Rust compiler enforces these rules at compile time, preventing a whole class of memory-related bugs.
You're probably thinking, "If ownership is always moved, how can I use a value in multiple places without making copies all the time?" That's the perfect question, and it leads directly to our next topic. In the last lesson, you saw &str as a "borrowed" view. In the next lesson, we will formalize this concept by exploring borrowing and references, Rust's solution for accessing data without taking ownership.