Ownership
Rust’s ownership system is the language’s defining feature. It replaces garbage collection with compile-time rules that guarantee memory safety and prevent data races.
The Three Rules
- Each value has exactly one owner (a variable)
- When the owner goes out of scope, the value is dropped (freed)
- There can only be one owner at a time
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1's ownership MOVES to s2
// println!("{s1}"); // Error: s1 no longer valid
println!("{s2}"); // Works fine
}
Move Semantics
Assignment transfers ownership for heap-allocated types. This is a move, not a copy.
fn take_ownership(s: String) {
println!("{s}");
} // s is dropped here, memory freed
fn main() {
let name = String::from("Alice");
take_ownership(name);
// name is no longer valid here - it was moved into the function
}
Tip: Types that implement
Copy(integers, booleans, floats, chars) are copied instead of moved. Stack-only data is cheap to duplicate.
let x = 42;
let y = x; // x is copied, not moved
println!("{x}"); // both still valid
println!("{y}");
Borrowing with References
Instead of transferring ownership, you can borrow a value with references.
Shared references (&T) - read-only
fn print_length(s: &String) {
println!("Length: {}", s.len());
} // s goes out of scope, but it doesn't own the String, so nothing is dropped
fn main() {
let name = String::from("Alice");
print_length(&name); // borrow name
println!("{name}"); // still valid - we only lent it
}
Mutable references (&mut T) - read and write
fn add_greeting(s: &mut String) {
s.push_str(", hello!");
}
fn main() {
let mut name = String::from("Alice");
add_greeting(&mut name);
println!("{name}"); // "Alice, hello!"
}
The Borrowing Rules
These two rules prevent data races at compile time:
| Rule | What it means |
|---|---|
Many &T OR one &mut T | You can have multiple readers or one writer, never both |
| References must be valid | A reference cannot outlive the data it points to |
let mut data = vec![1, 2, 3];
let first = &data[0]; // shared borrow
// data.push(4); // Error: can't mutate while shared borrow exists
println!("{first}"); // shared borrow used here
data.push(4); // OK: shared borrow is no longer in use
Gotcha: The borrow checker tracks where references are last used, not where they go out of scope. This is called Non-Lexical Lifetimes (NLL) and is why the example above compiles.
Common Pattern: Take &str Not String
Functions that only need to read string data should accept &str. This works with both String and string literals.
// Bad: forces caller to give up ownership or clone
fn greet_bad(name: String) {
println!("Hello, {name}");
}
// Good: borrows a string slice, works with &String and &str
fn greet(name: &str) {
println!("Hello, {name}");
}
fn main() {
let owned = String::from("Alice");
greet(&owned); // &String coerces to &str
greet("Bob"); // &str literal works directly
println!("{owned}"); // owned is still valid
}
Fighting the Borrow Checker
When the borrow checker rejects your code, the fix is usually one of these:
| Problem | Solution |
|---|---|
| Need the value in two places | Clone it (.clone()) or restructure to use references |
| Mutating while iterating | Collect indices first, then mutate. Or use .retain() / .iter_mut() |
| Self-referential struct | Use indices instead of references, or use Pin |
| Complex shared ownership | Use Rc<T> (single-thread) or Arc<T> (multi-thread) |
// Clone when ownership semantics get complicated
let config = load_config();
let config_for_server = config.clone();
let config_for_logger = config.clone();
// Rc for shared ownership without cloning data
use std::rc::Rc;
let shared = Rc::new(expensive_data());
let ref1 = Rc::clone(&shared); // cheap reference count bump
let ref2 = Rc::clone(&shared);
Tip: Reaching for
.clone()is perfectly fine when you’re learning. Optimize ownership patterns later once the code works. The compiler’s suggestions are usually good - read the error messages carefully.
Why This Matters
Ownership eliminates entire categories of bugs at compile time:
- No double frees - only one owner drops the value
- No dangling pointers - references can’t outlive data
- No data races - mutable access is exclusive
- No null pointer dereferences - Rust has no null (uses
Optioninstead) - No use-after-free - moved values can’t be accessed
These guarantees hold without runtime overhead. No garbage collector, no reference counting (unless you opt into Rc/Arc).
Next: Traits | Error Handling