Ownership & Borrowing
Ownership is Rust's most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector.
What is Ownership?
Ownership is a set of rules that govern how a Rust program manages memory. Unlike languages with garbage collection (like Java or Python) or manual memory management (like C), Rust uses a third approach: memory is managed through a system of ownership with rules that the compiler checks at compile time.
The Three Rules of Ownership
- 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
fn main() {
// s comes into scope
let s = String::from("hello");
// s is valid from this point forward
println!("{}", s);
} // s goes out of scope and is dropped
The Stack and the Heap
To understand ownership, we need to understand how data is stored:
- Stack: Fast, fixed-size data (integers, booleans, references)
- Heap: Dynamic-size data (Strings, Vectors, etc.)
fn main() {
// i32 is stored on the stack - copy is cheap
let x = 5;
let y = x; // y is a copy of x
println!("x = {}, y = {}", x, y); // Both are valid!
// String is stored on the heap
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 is no longer valid
println!("{}", s2); // Only s2 is valid
}
Move vs Copy
Types that have a known size at compile time implement the Copy trait:
fn main() {
// These types implement Copy
let a: i32 = 5;
let b = a; // copy
println!("a = {}, b = {}", a, b); // Both valid
// Strings do NOT implement Copy
let s1 = String::from("hello");
let s2 = s1; // move, not copy
// s1 is no longer valid here
}
Borrowing with References
Instead of taking ownership, you can borrow a value using references:
fn main() {
let s1 = String::from("hello");
// &s1 creates a reference to s1
let len = calculate_length(&s1);
// s1 is still valid because we only borrowed it!
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the String
Mutable References
By default, references are immutable. To modify borrowed data, use mutable references:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // Prints "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
The Rules of References
- You can have either one mutable reference or any number of immutable references
- References must always be valid (no dangling references)
fn main() {
let mut s = String::from("hello");
// Multiple immutable references are OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// After r1 and r2 are no longer used, we can have a mutable reference
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
}
Preventing Data Races
This restriction prevents data races at compile time:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot have two mutable references
println!("{}", r1);
}
The Slice Type
Slices let you reference a contiguous sequence of elements rather than the whole collection:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // or &s[..5]
let world = &s[6..11]; // or &s[6..]
println!("{} {}", hello, world);
}
Practice Exercise
Try this code in the playground:
fn main() {
let mut message = String::from("Hello");
// Borrow immutably to get length
let length = get_length(&message);
println!("Length: {}", length);
// Borrow mutably to modify
append_world(&mut message);
println!("Message: {}", message);
}
fn get_length(s: &String) -> usize {
s.len()
}
fn append_world(s: &mut String) {
s.push_str(", World!");
}
Key Takeaways
- Ownership ensures memory safety without a garbage collector
- Each value has exactly one owner
- When the owner goes out of scope, the value is dropped
- References allow borrowing without taking ownership
- You can have one mutable reference OR multiple immutable references
- The borrow checker enforces these rules at compile time
Understanding ownership is fundamental to writing safe, efficient Rust code!
Next Steps
Now that you understand ownership, you're ready to learn about structs and enums — Rust's tools for building custom data types that work hand in hand with the ownership system. You'll see how they let you model your domain precisely while the borrow checker keeps the data safe.
Next lesson
Structs & Enums
Learn how to define Rust structs and enums to create custom data types, implement methods, and model your domain effectively
20 min