Understanding Ownership
Ownership is Rust's most unique feature and the foundation of memory safety without garbage collection. Understanding ownership is crucial for writing effective Rust code and unlocks the language's guarantees around memory safety and concurrency.
What is Ownership?
Ownership is a set of rules that govern how Rust manages memory. Unlike languages with garbage collectors or manual memory management, Rust uses ownership to automatically manage memory at compile time, preventing memory leaks and use-after-free errors.
The Ownership Rules
Rust's ownership system is built on three fundamental rules:
- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped
Let's explore each rule in detail.
Rule 1: Each Value Has an Owner
Every piece of data in Rust has exactly one variable that "owns" it:
fn main() {
let s = String::from("hello"); // s owns the String
// The String data is owned by s
}
The variable s
is the owner of the String
containing "hello".
Rule 2: Only One Owner at a Time
A value can have only one owner at any given time:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves from s1 to s2
// println!("{}", s1); // Error! s1 no longer owns the data
println!("{}", s2); // This works fine
}
When we assign s1
to s2
, ownership of the String moves to s2
. This is called a move.
Rule 3: Values are Dropped When Owner Goes Out of Scope
When a variable goes out of scope, Rust automatically calls the drop
function to clean up the memory:
fn main() {
{
let s = String::from("hello"); // s comes into scope
// Do stuff with s
} // s goes out of scope and is dropped here
// s is no longer valid here
}
The Stack vs The Heap
Understanding the difference between stack and heap is crucial for ownership:
Stack Data
Data with a known, fixed size at compile time is stored on the stack:
fn main() {
let x = 5; // i32 stored on stack
let y = x; // Copy the value, both x and y are valid
println!("x: {}, y: {}", x, y); // Both work fine
}
Simple scalar types implement the Copy
trait, so they're copied rather than moved.
Heap Data
Data with unknown size at compile time or that might change is stored on the heap:
fn main() {
let s1 = String::from("hello"); // String data stored on heap
let s2 = s1; // Ownership moves to s2
// s1 is no longer valid
}
Copy vs Move Semantics
Rust has two ways to transfer values: copy and move.
Copy Types
Types that implement the Copy
trait are copied when assigned:
fn main() {
// These types implement Copy
let x = 5; // i32
let y = true; // bool
let z = 'a'; // char
let tuple = (1, 2); // Tuple of Copy types
let a = x; // x is copied, both x and a are valid
println!("x: {}, a: {}", x, a);
}
Types that implement Copy:
- All integer types (
i32
,u64
, etc.) - Boolean type (
bool
) - Character type (
char
) - Floating point types (
f32
,f64
) - Tuples containing only Copy types
Move Types
Types that don't implement Copy
are moved when assigned:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error: value borrowed here after move
println!("{}", s2); // This works
}
Functions and Ownership
Passing values to functions follows the same ownership rules:
Moving into Functions
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is moved into the function
// s is no longer valid here
// println!("{}", s); // Error!
let x = 5;
makes_copy(x); // x is copied (i32 implements Copy)
println!("{}", x); // x is still valid
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string goes out of scope and is dropped
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer goes out of scope, but nothing special happens
Returning Values and Ownership
fn main() {
let s1 = gives_ownership(); // Function moves its return value into s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 is moved into the function,
// which also moves its return value into s3
// s1 and s3 are valid here, s2 is not
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // Return value is moved out
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // Return value is moved out
}
The Problem with Move Semantics
Moving values can be inconvenient when you want to use a value after passing it to a function:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(s1); // s1 is moved
// println!("{}", s1); // Error! s1 is no longer valid
println!("Length: {}", len);
}
fn calculate_length(s: String) -> usize {
s.len()
} // s is dropped here
This is where references and borrowing come in, which we'll cover in the next section.
Clone for Deep Copying
If you want to deeply copy heap data, use the clone
method:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy the heap data
println!("s1: {}, s2: {}", s1, s2); // Both are valid
}
Note: Cloning can be expensive as it copies all the heap data.
Ownership in Different Contexts
With Structs
struct User {
username: String,
email: String,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("someusername123"),
email: String::from("[email protected]"),
active: true,
};
// This moves the String fields
let user2 = User {
username: user1.username, // username moved from user1
email: String::from("[email protected]"),
active: user1.active, // bool implements Copy
};
// user1 is partially moved - can't use the whole struct
// println!("{}", user1.username); // Error!
println!("{}", user1.active); // This still works (Copy type)
}
With Collections
fn main() {
let mut v = Vec::new();
v.push(String::from("hello"));
v.push(String::from("world"));
let first = v[0]; // Error! Can't move out of index
// Use indexing with references instead
let first = &v[0]; // Borrow instead of move
println!("{}", first);
// Or use methods that take ownership
let first = v.into_iter().next().unwrap(); // Consumes the vector
}
Ownership and Loops
Be careful with ownership in loops:
fn main() {
let strings = vec![
String::from("hello"),
String::from("world"),
];
// This moves each string out of the vector
for s in strings { // strings is moved here
println!("{}", s);
}
// println!("{:?}", strings); // Error! strings has been moved
// To avoid moving, use references
let strings = vec![
String::from("hello"),
String::from("world"),
];
for s in &strings { // Borrow instead of move
println!("{}", s);
}
println!("{:?}", strings); // strings is still valid
}
Box and Heap Allocation
Box<T>
provides a way to store data on the heap with a known size:
fn main() {
let b = Box::new(5); // Allocate an i32 on the heap
println!("b = {}", b);
let large_box = Box::new([0; 1_000_000]); // Large array on heap
// Box owns the heap data and will clean it up when dropped
}
Ownership Patterns
The RAII Pattern
Resource Acquisition Is Initialization - resources are tied to object lifetime:
use std::fs::File;
fn main() {
let file = File::open("example.txt").unwrap();
// File is automatically closed when `file` goes out of scope
}
Transfer of Ownership Pattern
fn process_string(s: String) -> String {
// Process the string
format!("Processed: {}", s)
}
fn main() {
let original = String::from("hello");
let processed = process_string(original); // Transfer ownership
// original is no longer valid, but processed is
}
Common Ownership Mistakes
Using Moved Values
fn main() {
let s = String::from("hello");
let s2 = s; // s is moved
// println!("{}", s); // Error: borrow of moved value
}
Partial Moves in Structs
struct Point {
x: String,
y: String,
}
fn main() {
let p = Point {
x: String::from("1"),
y: String::from("2"),
};
let x = p.x; // Partial move
// println!("{}", p.y); // Error: cannot use p.y after partial move
}
Benefits of Ownership
- Memory Safety: No use-after-free, double-free, or memory leaks
- No Runtime Overhead: All checks happen at compile time
- Thread Safety: Ownership prevents data races
- Predictable Performance: No garbage collection pauses
Mental Model for Ownership
Think of ownership like having a unique key to a resource:
- Only one person can hold the key at a time
- When you give the key to someone else, you no longer have access
- When the key holder leaves, the resource is automatically cleaned up
- You can make copies of the key for some simple resources (Copy types)
Understanding ownership is fundamental to mastering Rust. It enables memory safety without garbage collection and forms the foundation for Rust's powerful concurrency features. In the next sections, we'll explore how borrowing and references provide flexibility within the ownership system.