04. Ownership and Borrowing
📋 Jump to Takeaways🎁 You assign a variable to another one and the original stops working. No runtime crash, no null pointer — the compiler flat-out refuses to compile. Why would a language do this on purpose?
The Three Ownership Rules
Rust's memory safety comes from three rules enforced at compile time:
- Each value in Rust has exactly one owner (a variable).
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (memory freed).
fn main() {
let s = String::from("hello"); // s owns the String
println!("{}", s); // hello
} // s goes out of scope — memory is freed automaticallyNo garbage collector. No manual free(). The compiler inserts cleanup code at the exact right point.
Move Semantics
When you assign a heap-allocated value to another variable, Rust moves ownership. The original variable becomes invalid.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: value borrowed after move
println!("{}", s2); // hello
}The compiler error:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
3 | let s2 = s1;
| -- value moved here
4 | println!("{}", s1);
| ^^ value borrowed here after moveThis isn't a bug — it's the design. Two variables can't own the same heap data because then who frees it? Rust eliminates double-free by making moves the default.
Why Moves Exist
A String is stored as a pointer, length, and capacity on the stack, pointing to data on the heap. If Rust just copied the stack data (like C would), both s1 and s2 would point to the same heap memory. When both go out of scope, the memory gets freed twice — a double-free bug.
Rust's solution: after let s2 = s1, the compiler considers s1 invalid. One owner, one free, zero bugs.
Clone: Explicit Deep Copy
When you actually need a copy, call .clone() to explicitly duplicate the heap data.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — both are valid
println!("s1: {}", s1); // s1: hello
println!("s2: {}", s2); // s2: hello
}.clone() is intentionally verbose. It signals "this allocates memory" so you can spot performance costs at a glance.
The Copy Trait: Stack Types Are Different
Simple types that live entirely on the stack implement the Copy trait. Assignment copies them instead of moving.
fn main() {
let x = 5;
let y = x; // copy, not move — both valid
println!("x: {}, y: {}", x, y); // x: 5, y: 5
}Types that implement Copy: all integers (i32, u8, etc.), f64, bool, char, and tuples of Copy types. String does NOT implement Copy because it allocates on the heap.
Ownership and Functions
Passing a value to a function moves it, just like assignment. The function takes ownership.
fn take_ownership(s: String) {
println!("{}", s);
} // s is dropped here
fn main() {
let greeting = String::from("hi");
take_ownership(greeting);
// println!("{}", greeting); // ERROR: greeting was moved
}After calling take_ownership, you can't use greeting anymore. The function consumed it.
References: Borrowing Without Ownership
What if you want a function to read a value without taking it? Use a reference (&). This is called borrowing.
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it doesn't own the String, nothing is dropped
fn main() {
let greeting = String::from("hello");
let len = calculate_length(&greeting); // borrow, don't move
println!("'{}' has length {}", greeting, len);
// 'hello' has length 5
}The & creates a reference that borrows the value. You can use greeting afterwards because ownership never transferred.
(In practice, prefer &str over &String as a parameter type — it accepts both String and string literals. You'll see why in the Collections lesson.)
Mutable References
By default, references are immutable — you can look but not touch. To modify borrowed data, use &mut.
fn add_world(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let mut greeting = String::from("hello");
add_world(&mut greeting);
println!("{}", greeting); // hello, world!
}The variable must be declared mut, and the reference must be &mut. Both sides agree to the mutation.
The One Mutable Reference Rule
Rust enforces: you can have either one mutable reference OR any number of immutable references — never both at the same time.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK — immutable borrow
let r2 = &s; // OK — another immutable borrow
println!("{} {}", r1, r2); // hello hello
let r3 = &mut s; // OK — r1 and r2 are no longer used after this point
r3.push_str("!");
println!("{}", r3); // hello!
}But this fails:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ERROR: cannot borrow as mutable because it's also borrowed as immutable
println!("{}", r1);
}error[E0502]: cannot borrow `s` as mutable because it is also
borrowed as immutableThis rule prevents data races at compile time. If someone is reading, nobody can write. If someone is writing, nobody else can read or write.
Dangling References: Impossible in Rust
In C/C++, you can return a pointer to local data that gets freed — a dangling pointer. Rust won't let you:
// This does NOT compile
fn dangle() -> &String {
let s = String::from("hello");
&s // ERROR: s is dropped at end of function, reference would dangle
}error[E0106]: missing lifetime specifierThe fix: return the owned value instead.
fn no_dangle() -> String {
let s = String::from("hello");
s // move ownership out — no dangling reference
}
fn main() {
let s = no_dangle();
println!("{}", s); // hello
}Rust guarantees at compile time that references always point to valid data.
The Borrow Checker Summary
The borrow checker enforces these rules every time you compile:
- A value has exactly one owner
- A value is dropped when its owner leaves scope
- You can have unlimited
&T(shared/immutable references) OR exactly one&mut T(exclusive/mutable reference) — not both - References must always be valid (no dangling)
These rules eliminate: use-after-free, double-free, data races, and dangling pointers — all at compile time with zero runtime cost. (Null safety comes separately from the type system — Rust has no null, only Option<T>.)
Key Takeaways
- Each value has one owner; when the owner goes out of scope, the value is dropped
- Assigning a
Stringto another variable moves it — the original is invalidated - Use
.clone()for explicit deep copies when you need both variables alive - Stack-only types (
i32,bool,char) implementCopyand are duplicated on assignment &Tborrows without ownership — the original owner keeps the value&mut Tallows modification, but only one mutable reference can exist at a time- You cannot mix
&Tand&mut Treferences to the same data simultaneously - The compiler prevents dangling references — return owned values instead
- All of this is checked at compile time with zero runtime overhead
🎁 You can group related data and behavior together in Rust by building your own types. Next lesson: Structs and Methods — define a User with fields, attach methods to it, and see how ownership plays out when your types get more complex.