Understanding Rust Ownership: What Makes Rust Unique
Rust’s ownership model is what makes it stand out from other languages. Unlike languages that use garbage collection (like Java or Go), Rust enforces memory safety at compile time without needing a runtime.
Rust Ownership: The Heart of Its Memory Model
Rust’s ownership rules ensure memory is managed safely and efficiently, eliminating common bugs like dangling pointers, data races, or double frees.
The Three Ownership Rules
- Each value in Rust has a single owner.
- When the owner goes out of scope, the value is automatically dropped.
-
A value can be:
- moved,
- borrowed immutably (any number of times),
- or borrowed mutably (only once at a time).
Moving Ownership
fn main() {
let s1 = String::from("hello"); // s1 owns the String
let s2 = s1; // ownership moves to s2
// println!("{}", s1); // Error! s1 no longer owns the String
println!("{}", s2); // OK
}
Explanation:
s1
owns the string initially.- When assigned to
s2
, ownership is moved, not copied. s1
is now invalid; using it will cause a compile-time error.
🤁 Borrowing with References
If you want to access data without taking ownership, borrow it using references.
fn main() {
let s1 = String::from("hello");
print_length(&s1); // Pass a reference
println!("{}", s1); // Still valid!
}
fn print_length(s: &String) {
println!("Length is: {}", s.len());
}
Explanation:
&s1
is an immutable reference.- You can have multiple immutable references at the same time.
s1
retains ownership.
Mutable References (Only One at a Time)
fn main() {
let mut s = String::from("hello");
change(&mut s); // Mutable borrow
println!("{}", s);
}
fn change(s: &mut String) {
s.push_str(", world");
}
Explanation:
- Only one mutable reference is allowed at a time.
- This rule prevents data races at compile time.
What Happens If You Try Two Mutable Borrows?
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // Error: cannot borrow `s` as mutable more than once at a time
println!("{}, {}", r1, r2);
}
Rust will stop this code from compiling, ensuring safe access.
Lifetimes in Rust
Lifetimes are how Rust ensures that references are always valid. They don’t change how long data lives — they simply tell the compiler how long a reference is guaranteed to be valid.
Why Are Lifetimes Needed?
Here’s an example that doesn’t compile:
fn get_str() -> &String {
let s = String::from("hello");
&s // Error!
}
Error:
error[E0515]: cannot return reference to local variable `s`
This fails because s
is dropped when the function ends — returning a reference to it would be a dangling pointer.
Example Using Lifetimes
fn main() {
let s1 = String::from("apple");
let s2 = String::from("banana");
let result = longest(&s1, &s2);
println!("Longest: {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Explanation:
-
'a
is a lifetime annotation that says:- The returned reference will be valid as long as both input references are.
-
You must write lifetimes when the compiler can’t infer them — especially when returning references from a function.
Lifetimes
- Lifetimes help Rust verify reference safety at compile time.
- Most of the time, Rust infers lifetimes for you using lifetime elision rules.
-
You need to explicitly annotate lifetimes when:
- Returning references from functions.
- Working with structs that store references.
Summary
- Rust’s ownership and borrowing system manages memory without a garbage collector.
- You can move, borrow, or mutably borrow values, but under strict rules.
- Only one mutable reference allowed at a time to avoid data races.
- Lifetimes ensure references stay valid, especially when returning them from functions.
- Most of the time, you don’t need to write lifetimes manually — but when you do, they make your code safe and predictable.