Ownership and Borrowing | Rust
The Ownership Rules
Section titled “The Ownership Rules”Rust’s memory management rests on three rules enforced at compile time:
- Each value in Rust has a single owner.
- When the owner goes out of scope, the value is dropped (memory is freed).
- There can be zero or more immutable references (
&T) OR exactly one mutable reference (&mut T) to a value at any point in its lifetime.
These rules are checked by the borrow checker, which operates on MIR (Mid-level Intermediate Representation). The borrow checker does not exist at runtime — there is zero overhead for ownership Tracking in the compiled binary.
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is MOVED to s2 — s1 is no longer valid // println!("{}", s1); // ERROR: value borrowed after move println!("{}", s2); // OK — s2 owns the data}The move is a compile-time transfer of ownership. No memory is copied — only the pointer, length, And capacity (24 bytes for String on 64-bit) are copied. The original binding is invalidated.
Move Semantics
Section titled “Move Semantics”What Moves and What Copies
Section titled “What Moves and What Copies”Types are divided into two categories based on whether assignment copies or moves:
| Category | Examples | Behavior |
|---|---|---|
Copy types | i32``f64``bool``char``(i32, i32)``&T | Assignment copies the value |
| Move types | String``Vec<T>``Box<T>``FileUser-defined structs (unless Copy) | Assignment transfers ownership |
A type implements Copy if and only if every bit pattern of its memory representation is a valid Value. This is why types containing heap pointers (like String) cannot be Copy — a bitwise copy Would create two owners of the same heap allocation.
The Copy Trait
Section titled “The Copy Trait”#[derive(Copy, Clone)]struct Point { x: f64, y: f64,}
let p1 = Point { x: 1.0, y: 2.0 };let p2 = p1; // p1 is COPIED — both p1 and p2 are validprintln!("{} {}", p1.x, p2.y); // OKCopy requires Clone and is a marker trait with no methods. The compiler automatically implements Copy for types where all fields are Copy.
Types that cannot be Copy:
- Any type with a
Dropimplementation (destructor) - Any type containing a heap pointer (
String``Vec``Box) - Any type containing a mutable reference (
&mut T)
Partial Moves
Section titled “Partial Moves”Structs can be partially moved — individual fields can be moved out while other fields remain valid:
struct Person { name: String, age: u32,}
let person = Person { name: String::from("Alice"), age: 30,};
let name = person.name; // name is moved out of person// println!("{:?}", person); // ERROR: person partially movedprintln!("{}", person.age); // OK — age is Copy, was never movedAfter a partial move, the struct itself is no longer usable as a whole, but its Copy fields remain Accessible.
Moves in Function Calls
Section titled “Moves in Function Calls”Function arguments are moved by default:
fn takes_ownership(s: String) { println!("{}", s);} // s is dropped here
fn main() { let s = String::from("hello"); takes_ownership(s); // println!("{}", s); // ERROR: s was moved}To avoid the move, pass a reference:
fn borrows(s: &String) { println!("{}", s);}
fn main() { let s = String::from("hello"); borrows(&s); println!("{}", s); // OK — s was borrowed, not moved}Return Values Move Ownership
Section titled “Return Values Move Ownership”Functions transfer ownership to the caller via return values:
fn creates_ownership() -> String { String::from("hello") // ownership moves to caller}
fn takes_and_gives(s: String) -> String { s // ownership moves back to caller}References and Borrowing
Section titled “References and Borrowing”Immutable References
Section titled “Immutable References”An immutable reference &T allows reading but not modifying the referenced data. You can create any Number of immutable references simultaneously:
let s = String::from("hello");
let r1 = &s;let r2 = &s;let r3 = &s;println!("{} {} {}", r1, r2, r3); // OK — multiple immutable borrowsMutable References
Section titled “Mutable References”A mutable reference &mut T allows reading and modifying. Only one mutable reference can exist at a Time, and no immutable references can coexist with a mutable one:
let mut s = String::from("hello");
let r1 = &mut s;// let r2 = &mut s; // ERROR: cannot borrow as mutable more than oncer1.push_str(", world");println!("{}", r1);This is the core rule that prevents data races at compile time. The NLL (Non-Lexical Lifetimes) Borrow checker understands that r1 is no longer in use after its last usage point, not just at the End of the lexical scope:
let mut s = String::from("hello");
let r1 = &s; // immutable borrow startsprintln!("{}", r1); // r1 used here// r1's borrow ends here (NLL)
let r2 = &mut s; // OK — r1 is no longer in scoper2.push_str(", world");println!("{}", r2);Dangling Reference Prevention
Section titled “Dangling Reference Prevention”The borrow checker guarantees that references always point to valid data. This is one of Rust’s most Important safety guarantees:
fn dangle() -> &String { let s = String::from("hello"); &s // ERROR: s is created inside this function and will be dropped} // the reference would point to freed memory
fn no_dangle() -> String { let s = String::from("hello"); s // OK — ownership is transferred to the caller}The compiler error is: missing lifetime specifier — it is telling you that it cannot prove the Reference will outlive its referent.
Reference Rules Summary
Section titled “Reference Rules Summary”At any given lifetime scope for a value:
┌──────────────────────────────────────┐ │ &T &T &T (many immutable) │ ✓ │ &mut T (one mutable) │ ✓ │ &T &mut T (mixed) │ ✗ │ &mut T &mut T (multiple mutable) │ ✗ └──────────────────────────────────────┘Lifetimes
Section titled “Lifetimes”Lifetimes are Rust’s way of tracking how long a reference is valid. Every reference has a lifetime, But in most cases the compiler can infer it (lifetime elision rules). Explicit lifetime annotations Are needed when the compiler cannot determine the relationship between input and output lifetimes.
Lifetime Annotation Syntax
Section titled “Lifetime Annotation Syntax”Lifetimes are denoted with a leading apostrophe. By convention, 'a is the first lifetime, 'b the Second, etc.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}The annotation <'a> says: “there exists some lifetime 'a such that both x and y live at Least as long as 'aAnd the return value also lives at least as long as 'a.” The caller gets to Choose what 'a is, constrained by the actual lifetimes of the arguments.
Lifetime Elision Rules
Section titled “Lifetime Elision Rules”The compiler applies three rules to elide (omit) lifetime annotations. If after applying all three Rules, the compiler still cannot determine lifetimes, it errors.
Rule 1: Each parameter that is a reference gets its own lifetime parameter.
fn foo(x: &str) → fn foo<'a>(x: &'a str)fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str)Rule 2: If there is exactly one input lifetime parameter, that lifetime is assigned to all Output parameters.
fn foo(x: &str) -> &str → fn foo<'a>(x: &'a str) -> &'a strRule 3: If there are multiple input lifetime parameters but one of them is &self or &mut selfThe lifetime of self is assigned to all output parameters.
impl Foo { fn method(&self, x: &str) -> &str → fn method<'a, 'b>(&'a self, x: &'b str) -> &'a str}Lifetime Bounds
Section titled “Lifetime Bounds”Lifetimes can have bounds, just like type parameters:
// 'b must outlive 'a — 'b is at least as long as 'afn print<'a, 'b: "a>(x: &''b str, y: &"a str) { println!("{} {}", x, y);}This is useful when a struct holds a reference and you need to ensure the struct does not outlive The referent.
Struct Lifetimes
Section titled “Struct Lifetimes”When a struct holds a reference, you must annotate its lifetime:
struct Excerpt<'a> { part: &'a str,}
let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence;{ let words = novel.as_str(); let i = words.find('.').unwrap(); first_sentence = Excerpt { part: &words[..i] }; // Excerpt<'a> where 'a is the lifetime of words}// first_sentence is invalid here — words was droppedFunction Lifetimes
Section titled “Function Lifetimes”Lifetimes in function signatures establish relationships between input and output references. The Compiler does not change the actual lifetimes — it only verifies that the constraints are satisfied.
// The returned reference lives as long as the shorter of the two inputsfn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}
// The returned reference lives as long as x onlyfn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str { x}'static Lifetime
Section titled “'static Lifetime”'static means the reference lives for the entire duration of the program. All string literals have 'static lifetime:
let s: &'static str = "hello"; // embedded in the binaryIntuition
Section titled “Intuition”Ownership is Rust’s single most important concept. Imagine every value is a library book: only one person can check it out at a time. When you assign let s2 = s1, the book moves from s1 to s2, and s1 can no longer use it. References are like library cards: you can have many read-only cards (immutable borrows) or one write card (mutable borrow), but never both at once. This prevents two people from writing in the same book simultaneously. The borrow checker enforces these rules at compile time, so there is zero runtime cost.