Ownership
Overview
Ownership is Rust’s most distinctive feature—a system of rules that manages memory without garbage collection. It’s the foundation for Rust’s memory safety guarantees and eliminates entire classes of bugs at compile time.
flowchart TD
subgraph "Other Languages"
A["Manual Memory<br/>(C/C++)"] --> D["Memory leaks<br/>Use-after-free<br/>Double-free"]
B["Garbage Collection<br/>(Java, Go, Python)"] --> E["Unpredictable pauses<br/>Runtime overhead"]
end
subgraph "Rust"
C["Ownership System"] --> F["No GC pauses<br/>No memory bugs<br/>Zero-cost abstraction"]
end
style C fill:#90EE90
style F fill:#90EE90
Key insight: Ownership rules are checked at compile time, so there’s no runtime overhead. If your code compiles, memory is managed correctly.
When Ownership Matters
| Situation | What Happens | Your Action |
|---|---|---|
| Assigning heap data | Ownership moves | Clone if you need both |
| Passing to function | Ownership moves | Pass reference instead |
| Returning from function | Ownership transfers out | Just return the value |
| Going out of scope | Value is dropped | Nothing—automatic |
Working with Copy types |
Value is copied | Nothing—automatic |
flowchart TD
A{What type of data?} -->|"Stack (Copy types)"| B["i32, bool, char, etc."]
A -->|"Heap (owned types)"| C["String, Vec, Box, etc."]
B --> D["Copied on assignment<br/>Both variables valid"]
C --> E["Moved on assignment<br/>Original invalid"]
E --> F{Need original?}
F -->|Yes| G["Use .clone()"]
F -->|No| H["Let it move"]
style D fill:#90EE90
style G fill:#FFE4B5
The Three Rules of Ownership
These rules are enforced at compile time:
flowchart LR
subgraph "Rule 1"
R1["Each value has<br/>exactly ONE owner"]
end
subgraph "Rule 2"
R2["Only ONE owner<br/>at a time"]
end
subgraph "Rule 3"
R3["When owner goes<br/>out of scope,<br/>value is DROPPED"]
end
R1 --> R2 --> R3
fn main() {
{ // Scope begins
let s = String::from("hello"); // s is the owner
// s is valid and usable here
} // Scope ends - s is dropped
// s no longer exists, memory is freed
}
Stack vs Heap: Memory Layout
Understanding where data lives is crucial for understanding ownership:
flowchart LR
subgraph Stack["Stack (Fast, Fixed)"]
direction TB
S1["x: i32 = 5"]
S2["y: bool = true"]
S3["ptr ─────────────"]
S4["len: 5"]
S5["cap: 5"]
end
subgraph Heap["Heap (Flexible, Managed)"]
H1["'h' 'e' 'l' 'l' 'o'"]
end
S3 --> H1
| Stack | Heap |
|---|---|
| Fixed size, known at compile time | Dynamic size, can grow |
| Very fast allocation/deallocation | Slower allocation |
| Automatically cleaned up (LIFO) | Managed by ownership |
i32, bool, char, [T; N], tuples |
String, Vec<T>, Box<T>, HashMap |
Move Semantics
When you assign a heap value to another variable, ownership moves:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership MOVES from s1 to s2
// println!("{}", s1); // ❌ ERROR! s1 is no longer valid
println!("{}", s2); // ✓ Works! s2 is the owner
}
flowchart LR
subgraph Before["Before: let s2 = s1"]
A1["s1"] --> D1["ptr|len|cap"]
D1 --> H1["'hello'"]
end
subgraph After["After: s1 is INVALID"]
A2["s1 ❌"]
A3["s2"] --> D2["ptr|len|cap"]
D2 --> H2["'hello'"]
end
Before -->|"Move"| After
style A2 fill:#FFB6C1
Why Move Instead of Copy?
flowchart TD
A["If both s1 and s2 pointed<br/>to same heap data..."] --> B["Both go out of scope"]
B --> C["Double free! 💥"]
C --> D["Memory corruption"]
E["With move semantics..."] --> F["Only s2 owns the data"]
F --> G["Only s2 drops it"]
G --> H["Memory safe ✓"]
style C fill:#FFB6C1
style H fill:#90EE90
Copy Types
Simple stack-only types implement Copy and are copied, not moved:
fn main() {
let x = 5;
let y = x; // x is COPIED to y (not moved)
println!("x = {}, y = {}", x, y); // ✓ Both valid!
}
What Implements Copy?
flowchart TD
A{Is it Copy?} -->|"Integers"| Y["i8, i16, i32, i64, i128<br/>u8, u16, u32, u64, u128 ✓"]
A -->|"Floats"| Y2["f32, f64 ✓"]
A -->|"Boolean"| Y3["bool ✓"]
A -->|"Character"| Y4["char ✓"]
A -->|"Tuples of Copy"| Y5["(i32, bool) ✓"]
A -->|"Arrays of Copy"| Y6["[i32; 5] ✓"]
A -->|"Has heap data"| N["String, Vec ✗"]
A -->|"Implements Drop"| N2["Custom cleanup ✗"]
style Y fill:#90EE90
style Y2 fill:#90EE90
style Y3 fill:#90EE90
style Y4 fill:#90EE90
style Y5 fill:#90EE90
style Y6 fill:#90EE90
style N fill:#FFB6C1
style N2 fill:#FFB6C1
If a type implements Drop (custom cleanup), it cannot implement Copy. The two are mutually exclusive.
Clone: Explicit Deep Copy
For heap types, use clone() to create a deep copy:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy - both own separate data
println!("s1 = {}, s2 = {}", s1, s2); // ✓ Both valid!
}
flowchart LR
subgraph "After clone()"
A1["s1"] --> D1["ptr|len|cap"]
D1 --> H1["'hello'"]
A2["s2"] --> D2["ptr|len|cap"]
D2 --> H2["'hello'"]
end
H1 -.-|"Separate copies"| H2
clone() copies all heap data, which can be expensive for large structures. Use it intentionally.
Ownership and Functions
Passing a value to a function moves or copies it, just like assignment:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is MOVED into the function
// println!("{}", s); // ❌ ERROR! s is invalid
let x = 5;
makes_copy(x); // x is COPIED (i32 is Copy)
println!("x = {}", x); // ✓ Works! x is still valid
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s is dropped here - memory freed
fn makes_copy(x: i32) {
println!("{}", x);
} // x goes out of scope, nothing special happens
sequenceDiagram
participant main
participant takes_ownership
participant makes_copy
Note over main: let s = String::from("hello")
main->>takes_ownership: s (ownership moves)
Note over main: s is now INVALID
Note over takes_ownership: s dropped at end
Note over main: let x = 5
main->>makes_copy: x (value copied)
Note over main: x still valid ✓
Return Values and Ownership
Functions can transfer ownership back to the caller:
fn main() {
let s1 = gives_ownership(); // Ownership comes to s1
println!("{}", s1); // s1 owns "yours"
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 moves in, result comes to s3
// s2 is invalid, s3 owns the string
}
fn gives_ownership() -> String {
String::from("yours") // Ownership moves to caller
}
fn takes_and_gives_back(s: String) -> String {
s // Just return it - ownership moves to caller
}
flowchart LR
subgraph "gives_ownership()"
G1["Create String"] --> G2["Return to caller"]
end
subgraph "main"
M1["s1 receives ownership"]
end
G2 --> M1
The Problem: Tedious Ownership Transfers
Sometimes you want to use a value without taking ownership:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1); // Tedious: return the string back
println!("'{}' has length {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // Have to return both the string AND the result
}
This is where borrowing comes in! See the next chapter to learn how references solve this problem elegantly.
Ownership in Data Structures
Structs own their fields:
struct User {
name: String, // User owns this String
age: u32, // And this integer
}
fn main() {
let user = User {
name: String::from("Alice"),
age: 30,
};
let name = user.name; // Partial move! name field moved out
// println!("{}", user.name); // ❌ ERROR! name was moved
println!("{}", user.age); // ✓ Works! age wasn't moved
}
flowchart TD
subgraph "Partial Move"
U["user"]
U --> N["name: moved ❌"]
U --> A["age: 30 ✓"]
end
subgraph "name variable"
V["name owns 'Alice'"]
end
N -.->|"moved to"| V
style N fill:#FFB6C1
style A fill:#90EE90
Common Ownership Patterns
Pattern 1: Transfer In, Process, Transfer Out
fn process(mut data: Vec<i32>) -> Vec<i32> {
data.push(42);
data.sort();
data // Transfer ownership back
}
Pattern 2: Create and Return
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name) // New String, caller owns it
}
Pattern 3: Take Ownership to Consume
impl Connection {
fn close(self) { // Takes ownership - self consumed
// Connection is dropped at end
// Prevents use after close!
}
}
Pattern 4: Builder Pattern
impl StringBuilder {
fn append(mut self, s: &str) -> Self {
self.buffer.push_str(s);
self // Return ownership for chaining
}
fn build(self) -> String {
self.buffer // Consume builder, return result
}
}
// Usage: ownership flows through chain
let result = StringBuilder::new()
.append("Hello")
.append(" World")
.build();
Visualizing Ownership Flow
sequenceDiagram
participant main
participant String as String "hello"
main->>String: let s1 = String::from("hello")
Note over main: s1 is the owner
main->>main: let s2 = s1
Note over main: Ownership moves to s2
Note over String: Now owned by s2
main->>main: } // end of scope
Note over String: s2 dropped, memory freed
Common Mistakes and Fixes
Mistake 1: Using After Move
// ❌ WRONG
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // Error: value moved
// ✓ FIX: Clone if you need both
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{} {}", s1, s2); // Both valid
// ✓ BETTER: Use references (borrowing)
let s1 = String::from("hello");
let s2 = &s1; // Borrow, don't move
println!("{} {}", s1, s2);
Mistake 2: Returning Reference to Local
// ❌ WRONG
fn create() -> &String {
let s = String::from("hello");
&s // Error: s is dropped, reference would dangle
}
// ✓ FIX: Return owned value
fn create() -> String {
String::from("hello") // Ownership transfers to caller
}
Mental Model
Think of ownership like physical objects:
flowchart LR
subgraph "Physical World Analogy"
G["Give away (move)"] --> G1["You no longer have it"]
L["Lend (borrow)"] --> L1["Temporary, must return"]
C["Make a copy (clone)"] --> C1["Both have one"]
D["Destroy (drop)"] --> D1["When done with it"]
end
| Action | Physical | Rust |
|---|---|---|
| Give away | Hand over book | let s2 = s1; (move) |
| Lend | Let friend read | let r = &s1; (borrow) |
| Copy | Photocopy document | let s2 = s1.clone(); |
| Destroy | Throw in trash | Value goes out of scope |
Summary
mindmap
root((Ownership))
Rules
One owner
One at a time
Drop when out of scope
Move vs Copy
Heap types move
Stack types copy
Clone for deep copy
Functions
Move in
Move out
Or borrow
Memory
Stack - fast, fixed
Heap - flexible, owned
| Concept | Stack Types | Heap Types |
|---|---|---|
| Assignment | Copy | Move |
| Function param | Copy | Move |
| Need both values | Just use | Clone |
| Want to share | Just use | Borrow |
Exercises
- Predict which lines will compile:
let s1 = String::from("hello"); let s2 = s1; println!("{}", s1); println!("{}", s2); - Fix this code without changing the function signature:
fn main() { let s = String::from("hello"); print_string(s); println!("{}", s); } fn print_string(s: String) { println!("{}", s); } - Explain why
Vec<i32>doesn’t implementCopyeven thoughi32does.
See Also
Next Steps
Learn about Borrowing to use values without taking ownership.