Chapter 3: Ownership & Borrowing
Ownership is Rust's most distinctive feature and the foundation of its memory safety without a garbage collector. This chapter covers the three ownership rules, borrowing and the borrow checker, lifetimes, and slices. Once you understand it, you can read the compiler's errors and see why it asks what it asks.
Learning Objectives
- Master the three rules of ownership and move semantics.
- Understand borrowing: shared references
&Tand mutable references&mut T. - Know the borrowing rules and the "many references at once" restriction.
- Use lifetime annotations to make reference relationships explicit.
- Use slices
&[T]/&strto borrow a span of contiguous data.
3.1 The Three Rules of Ownership
Rust's memory management rests on three rules:
- Each value has a single owner — a variable.
- When the owner goes out of scope, the value is dropped (its destructor runs, memory is freed).
- Assignment or passing to a function moves ownership — unless the type implements
Copy.
fn main() { { let s = String::from("hello"); // s owns it println!("{s}"); } // s goes out of scope; the String's memory is freed — no free needed let s1 = String::from("hello"); let s2 = s1; // ownership moves from s1 to s2 // println!("{s1}"); // error: s1 was moved, no longer valid println!("{s2}"); }
Move vs Copy
String owns heap memory, so assignment is a move — the old variable is invalidated, avoiding a double free. Stack types (integers, booleans, chars, fixed-size arrays) implement the Copy trait, so assignment is a bitwise copy and the old variable stays usable:
fn main() { let a = 5; let b = a; // i32 is Copy; a still usable println!("{a} {b}"); let s1 = String::from("hi"); let s2 = s1; // String is not Copy; s1 is moved // println!("{s1}"); // error }
Passing to a function is a move too: after you pass a
Stringto a function, the caller can no longer use it. To "lend without transferring ownership," use references (next section).
3.2 Borrowing & References
Borrowing lets a function use a value without taking ownership. &T is a shared reference (read-only); &mut T is a mutable reference:
fn calculate_length(s: &String) -> usize { s.len() } // s is a borrow; nothing is freed here fn append(s: &mut String) { s.push_str("!"); } fn main() { let mut s = String::from("hello"); let len = calculate_length(&s); // borrow; s still owned by main println!("{s} length {len}"); append(&mut s); println!("{s}"); // hello! }
The Two Borrowing Rules
The borrow checker enforces two rules at compile time:
- At any moment, you may have either several shared references
&T, or exactly one mutable reference&mut T— not both. - References must always be valid (never dangling).
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; // OK: several shared references // let r3 = &mut s; // error: cannot borrow mutably while shared refs exist println!("{r1} {r2}"); let mut s = String::from("hi"); let r1 = &mut s; // let r2 = &mut s; // error: only one mutable reference at a time println!("{r1}"); }
Why so strict? Mixing mutable references is exactly what causes data races and iterator invalidation. Rejecting them at compile time eliminates a whole class of concurrency bugs up front.
NLL: Non-Lexical Lifetimes
The modern borrow checker (NLL) looks at where a reference is actually last used, not the end of its scope:
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{r1} {r2}"); // r1, r2 are no longer used after this point let r3 = &mut s; // OK: the old shared refs are no longer needed println!("{r3}"); }
3.3 Dangling References
A function cannot return a reference to a local variable — the variable is freed when the function returns, leaving the reference dangling. The compiler rejects it:
#![allow(unused)] fn main() { // fn dangle() -> &String { // let s = String::from("hi"); // &s // } // error: s is freed here; the returned reference would dangle // Correct: return the String, transferring ownership fn no_dangle() -> String { let s = String::from("hi"); s } }
3.4 Lifetimes
When references come from several places and the compiler cannot infer which lives longest, lifetime annotations spell out the relationship. They do not change how long a reference lives; they only declare constraints.
// 'a means: the returned reference lives at least as long as the shorter of x and y 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 s2 = String::from("hi"); let result = longest(s1.as_str(), s2.as_str()); println!("longer: {result}"); }
Lifetime elision
Most of the time you need not write annotations. The compiler applies three elision rules automatically:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it is assigned to all output references.
- If there is a
&self/&mut self,self's lifetime is assigned to all output references.
When these do not apply, the compiler errors and asks you to annotate explicitly — usually a sign your API needs rethinking.
Lifetimes in structs
A struct holding a reference must annotate it:
struct Excerpt<'a> { part: &'a str, } fn main() { let novel = String::from("call me Ishmael. some years ago..."); let first = novel.split('.').next().unwrap(); let e = Excerpt { part: first }; println!("{:?}", e.part); }
'a says an Excerpt cannot outlive the string it borrows.
3.5 Slices
A slice is a borrow of a contiguous sequence, without ownership. &[T] is a slice of an array/Vec; &str is a string slice:
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &b) in bytes.iter().enumerate() { if b == b' ' { return &s[..i]; } } &s[..] } fn main() { let s = String::from("hello world"); let word = first_word(&s); // word borrows s println!("{word}"); // If you mutated s here, the borrow checker would reject it because word is still alive. }
Slices let one function work for &String, &str, &[T], and &Vec<T> — a key to writing general Rust code.
3.6 Putting It Together
Tying ownership, borrowing, and slices together — a function that finds the longest line with no allocation:
fn longest_line<'a>(lines: &'a [&'a str]) -> Option<&'a str> { lines.iter().copied().max_by_key(|l| l.len()) } fn main() { let text = ["short", "a longer line", "mid"]; if let Some(longest) = longest_line(&text) { println!("longest: {longest}"); } }
longest_line only borrows the slice and returns a borrow — no heap allocation at all. That is zero-cost abstraction in action.
3.7 Summary
The three ownership rules, move semantics, the two borrowing rules, lifetime annotations, and slices form the skeleton of Rust's memory safety. The borrow checker is strict, but it eliminates null pointers, dangling references, double frees, and data races. It rejects not your intent but the bugs hiding in your code. Master this chapter and you have crossed Rust's steepest hill.
Exercises
- Explain why
let s2 = s1;(s1isString) invalidatess1, whilelet b = a;(aisi32) leavesausable. - Write
fn longest_word(s: &str) -> &strreturning the first longest word (split on spaces). Annotate any lifetimes needed and observe the elision rules. - Write a struct
Config<'a>holding a&strand construct an instance; verify it cannot outlive theStringit borrows.