PHASE 1 ← Back to Course
3 / 17
🔑

Ownership, Rust's Killer Feature

Rust's most unique feature, understand the rules that guarantee memory safety without garbage collection.

1

What is Ownership?

Ownership is Rust's most distinctive feature and is crucial to understanding how Rust ensures memory safety without garbage collection. It's also the source of most beginners' struggles with Rust. But once you understand it, you'll appreciate its power.

Ownership is a set of rules that governs how a Rust program manages memory. Every value in Rust has an owner, and when the owner goes out of scope, the value is dropped (memory is freed). This ensures memory is always properly cleaned up.

Stack vs. Heap

To understand ownership, it helps to understand how the stack and heap work:

Rust
fn main() {
    let x = 5;           // Small integer on stack
    let y = "hello";     // String literal (stack)
    let s = String::new();  // String on heap (pointer on stack)
}
2

Ownership Rules

Rust has three fundamental rules of ownership:

  1. Each value in Rust has a variable that's called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Key Insight

These three rules are the foundation of Rust's memory safety. The compiler enforces them at compile-time, so there is zero runtime cost for memory management.

3

Move Semantics

When you assign a value from one variable to another, ownership is transferred. This is called a "move":

Rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1's ownership moves to s2
    // This would cause a compile error:
    // println!("{}", s1);  // Error: s1 has been moved
    println!("{}", s2);  // This works fine
}

Why Move Semantics?

For simple types like integers, Rust copies the value instead of moving. This is because they're stored on the stack and are cheap to copy:

Rust
fn main() {
    let x = 5;
    let y = x;  // x is copied (integers implement Copy)
    println!("x = {}, y = {}", x, y);  // Both are valid
}

Clone: Explicit Deep Copy

When you want to make a copy of heap data, explicitly call clone():

Rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // Explicit deep copy
    println!("s1 = {}, s2 = {}", s1, s2);  // Both are valid
}

Copy Trait

Types that implement the Copy trait are automatically copied instead of moved. Simple types like integers, floats, booleans, and characters implement Copy:

Rust
fn main() {
    let x = 42;        // i32 implements Copy
    let y = x;          // x is copied automatically
    let z: bool = true;  // bool implements Copy
    let w = z;          // z is copied automatically
    println!("x={}, y={}, z={}, w={}", x, y, z, w);
}
4

Functions and Ownership

When you pass a value to a function, ownership is transferred (or copied):

Rust
fn take_ownership(s: String) {
    println!("I own: {}", s);
}  // s is dropped here
fn copy_integer(x: i32) {
    println!("I have: {}", x);
}  // x is not dropped because i32 implements Copy
fn main() {
    let s = String::from("hello");
    take_ownership(s);  // s's ownership moves into function
    // This would error: println!("{}", s);  // s no longer valid
    let num = 5;
    copy_integer(num);  // num is copied
    println!("num = {}", num);  // num is still valid
}

Functions can also return ownership:

Rust
fn give_ownership() -> String {
    String::from("hello")
fn take_and_give_back(s: String) -> String {
    s  // Return ownership
fn main() {
    let s1 = give_ownership();
    let s2 = String::from("world");
    let s3 = take_and_give_back(s2);  // s2 moves in, s3 receives ownership
    println!("s1 = {}, s3 = {}", s1, s3);
}
5

References and Borrowing

Instead of transferring ownership, you can lend a reference to a value. A reference allows you to use a value without taking ownership:

Immutable References

Rust
fn calculate_length(s: &String) -> usize {
    s.len()
}  // s goes out of scope, but it doesn't own s, so nothing happens
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // Borrow s1
    println!("'{}' has length {}", s1, len);  // s1 is still valid
}

Mutable References

You can modify a value through a mutable reference:

Rust
fn change_string(s: &mut String) {
    s.push_str(" world");
fn main() {
    let mut s = String::from("hello");
    change_string(&mut s);  // Mutable borrow
    println!("{}", s);  // Prints: hello world
}
⚠️

Borrowing Rules

At any given time, you can have EITHER one mutable reference OR any number of immutable references. You cannot have both.

Rust
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;      // Immutable reference
    let r2 = &s;      // Another immutable reference (OK)
    println!("{}, {}", r1, r2);
    // This would error (can't mix mutable and immutable):
    // let r3 = &mut s;
    let r3 = &mut s;  // Mutable reference (now OK after r1, r2 are gone)
    r3.push_str(" world");
    println!("{}", r3);
}
6

The Slice Type

A slice is a reference to a contiguous sequence of elements within a collection. Slices don't take ownership:

String Slices

Rust
fn main() {
    let s = String::from("Hello World");
    let hello = &s[0..5];   // "Hello"
    let world = &s[6..11];  // "World"
    println!("{} {}", hello, world);
}

Array Slices

Rust
fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..4];  // [2, 3, 4]
    for &item in slice {
        println!("Item: {}", item);
    }
}

Practical Function with Slices

Rust
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]  // Return the whole string
fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);
    println!("First word: {}", word);  // Prints: First word: hello
}
7

Dangling References

Rust prevents dangling references, references to data that no longer exists. The compiler catches these at compile-time:

Rust
fn dangle() -> &String {  // This won't compile!
    let s = String::from("hello");
    &s  // s is dropped at end of function, so reference is invalid
}  // ERROR: `s` does not live long enough
fn no_dangle() -> String {  // This works!
    let s = String::from("hello");
    s  // Ownership is transferred, not a reference
fn main() {
    let s = no_dangle();
    println!("{}", s);
}
💡

The Compiler Has Your Back

Rust's borrow checker catches dangling references at compile-time. In C or C++, these would be runtime bugs that are notoriously difficult to track down.

8

Ownership Summary

Ownership is powerful once you understand it. Here's a practical example that brings everything together:

Rust
fn main() {
    let mut text = String::from("Ownership");
    // Immutable borrow - we can read
    let len = get_length(&text);
    println!("Length: {}", len);
    // Mutable borrow - we can modify
    add_exclamation(&mut text);
    println!("Text: {}", text);
    // Transfer ownership
    let owned = text;
    println!("Owned: {}", owned);
    // text is no longer valid here
fn get_length(s: &String) -> usize {
    s.len()
fn add_exclamation(s: &mut String) {
    s.push_str("!!!");
}

Ownership Cheat Sheet

  • Move: Ownership transfers from one variable to another (let s2 = s1;)
  • Clone: Explicit deep copy of heap data (let s2 = s1.clone();)
  • Copy: Automatic copy for stack-only types (integers, bools, chars)
  • Borrow: Lend a reference without giving up ownership (&s or &mut s)
  • Slice: Reference to a portion of a collection (&s[0..5])
← Previous Chapter 3 of 17 Next →