Understanding Ownership and Borrowing in Rust
Master Rust's most distinctive feature — ownership — and learn how it enables memory safety without a garbage collector.
Table of Contents
Understanding Ownership and Borrowing in Rust
Rust's ownership system is what makes it unique among systems programming languages. It enforces memory safety at compile time, eliminating entire classes of bugs like use-after-free, double-free, and data races — without a garbage collector.
What Is Ownership?
Every value in Rust has a single owner — a variable that holds that value. When the owner goes out of scope, Rust automatically frees the memory.
fn main() {
let s = String::from("hello"); // s owns the String
println!("{}", s);
} // s drops here — memory freed automatically
The Three Rules of Ownership
Move Semantics
When you assign a heap-allocated value to another variable, ownership moves. The original variable becomes invalid:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2
// println!("{}", s1); // ❌ compile error: s1 no longer valid
println!("{}", s2); // ✅ works fine
}
Stack-allocated types (like integers, booleans, f64) implement the Copy
trait and are copied instead of moved:
fn main() {
let x = 5;
let y = x; // x is copied, not moved
println!("x={}, y={}", x, y); // ✅ both valid
}
Borrowing
Instead of transferring ownership, you can borrow a value using references:
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope but doesn't drop the String (it doesn't own it)
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // borrow s1
println!("'{}' has {} characters", s1, len); // ✅ s1 still valid
}
Mutable References
To modify a borrowed value, use &mut:
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello, world"
}
The Borrow Checker
The borrow checker enforces borrowing rules:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // immutable borrow
let r2 = &s; // another immutable borrow — OK
// let r3 = &mut s; // ❌ cannot borrow mutably while immutably borrowed
println!("{} and {}", r1, r2);
// r1 and r2 no longer used after this point
let r3 = &mut s; // ✅ OK now — previous borrows ended
r3.push_str("!");
println!("{}", r3);
}
Lifetimes
Lifetimes are Rust's way of ensuring references don't outlive the data they point to:
// 'a is a lifetime annotation: output lives at least as long as input
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result); // ✅ both in scope
}
}
Summary
| Concept | What it means |
|---|---|
| Ownership | Each value has one owner; dropped when owner leaves scope |
| Move | Ownership transfers; original variable invalidated |
| Copy | Stack types are copied, not moved |
Borrow (&T) |
Immutable reference; multiple allowed |
Borrow (&mut T) |
Mutable reference; only one at a time |
Lifetime ('a) |
Ensures references are always valid |
Rust's ownership model takes getting used to, but it pays dividends: you get memory safety, no garbage collector pauses, and thread safety — all guaranteed at compile time.