Rust Ownership and Borrowing
Ownership is Rust's revolutionary memory management system that enforces strict rules at compile time to guarantee memory safety without garbage collection. Combined with borrowing, these concepts prevent common bugs like dangling pointers, data races, and memory leaks while maintaining performance.
1. The Ownership System
Core Rules:
- Each value has a single owner variable
- When the owner goes out of scope, the value is dropped
- Assignment transfers ownership (moves) by default
fn main() {
// String owns heap-allocated memory
let s1 = String::from("hello");
// Ownership moves to s2 - s1 is no longer valid
let s2 = s1;
// ERROR: value borrowed here after move
// println!("{}", s1);
// Ownership in functions
takes_ownership(s2); // s2 moves into function
// println!("{}", s2); // Would error - s2 invalid
let x = 5;
makes_copy(x); // i32 is Copy - still valid
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s dropped here
fn makes_copy(n: i32) {
println!("{}", n);
}
Key Concepts:
- Heap-allocated types (String, Vec) implement move semantics
- Stack-allocated types (i32, bool) implement Copy trait
- Scope boundaries determine when values are dropped
2. References and Borrowing
Borrowing Rules:
- You can have either:
- One mutable reference, OR
- Any number of immutable references
- References must always be valid
fn main() {
let s = String::from("hello");
// Immutable borrow
let len = calculate_length(&s);
println!("Length of '{}': {}", s, len);
// Mutable borrow
let mut s = String::from("hello");
change_string(&mut s);
println!("Modified: {}", s);
}
fn calculate_length(s: &String) -> usize {
s.len()
// s goes out of scope but doesn't drop the String
}
fn change_string(s: &mut String) {
s.push_str(", world!");
}
Why This Matters:
- Prevents data races at compile time
- Enables safe concurrent programming
- No runtime overhead like garbage collection
3. Slices - View Into Data
What They Are: References to contiguous sequences in collections without ownership.
Key Benefit: Safe access to portions of arrays/strings with bounds checking.
fn main() {
let s = String::from("hello world");
// String slice
let hello = &s[0..5];
let world = &s[6..11];
println!("{} {}", hello, world);
// Array slice
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]
// Best practice: accept &str instead of &String
print_slice(&s[..]); // Whole string as slice
}
fn print_slice(s: &str) {
println!("Slice: {}", s);
}
Slice Rules:
- Don't own the data they reference
- Bounds checked at compile time where possible
- More flexible than full collection references
4. Ownership Patterns
Returning Ownership: Functions can transfer ownership back to caller.
Struct Ownership: Fields own their data unless using references with lifetimes.
// 1. Returning ownership
fn create_string() -> String {
let s = String::from("new string");
s // Ownership returned
}
// 2. Struct ownership
struct Library {
books: Vec<String>, // Owns book titles
}
impl Library {
fn add_book(&mut self, title: String) {
self.books.push(title); // Ownership moved to struct
}
}
// 3. Avoiding moves with references
fn process_data(data: &[i32]) {
// Work with borrowed data
}
Common Strategies:
- Use borrowing where possible to avoid moves
- Clone data only when necessary
- Design APIs to minimize ownership transfers
5. Lifetime Annotations
What They Do: Specify how long references must remain valid.
When Needed: When functions return references or structs contain references.
// Function with explicit lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Struct containing reference
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find sentence");
let excerpt = Excerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
}
Lifetime Elision Rules:
- Each parameter gets its own lifetime if unspecified
- If exactly one input lifetime, it's assigned to all outputs
- For methods,
&selfor&mut selfgets priority
6. Shared Ownership with Smart Pointers
When Needed: For scenarios requiring multiple owners or heap allocation.
use std::rc::Rc;
use std::cell::RefCell;
// 1. Reference Counting (single-threaded)
let rc = Rc::new(String::from("shared"));
let rc_clone = Rc::clone(&rc);
// 2. RefCell for interior mutability
let data = RefCell::new(42);
{
let mut borrow = data.borrow_mut();
*borrow += 1;
}
// 3. Arc for thread-safe reference counting
use std::sync::Arc;
let arc = Arc::new(5);
let arc_clone = Arc::clone(&arc);
std::thread::spawn(move || {
println!("{}", arc_clone);
});
Pointer Types:
Box<T>: Simple heap allocationRc<T>: Single-threaded reference countingArc<T>: Thread-safe atomic reference countingRefCell<T>: Interior mutability with runtime checks