What is Ownership?
Ownership is Rust’s most unique feature and the foundation of its memory management system. It’s a set of rules that the compiler checks at compile time to ensure memory safety without a garbage collector.
The three core rules of ownership in Rust are:
- Each value in Rust has a variable that is its “owner”
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped (memory freed)
Let’s see ownership in action:
fn main() {
// s1 is the owner of the String value
let s1 = String::from("hello");
// Ownership moves from s1 to s2
let s2 = s1;
// This would cause a compile error because s1 no longer owns the value
// println!("s1: {}", s1); // Error: value borrowed after move
// s2 is valid and now owns the String
println!("s2: {}", s2);
// When we exit this scope, s2 is dropped and memory is freed
} // s2 goes out of scope and memory is freed
This system allows Rust to guarantee memory safety without a garbage collector, as it knows exactly when to free memory at compile time.
Move Semantics
In Rust, when you assign a value from one variable to another, the ownership moves to the new variable. This is called “moving” a value, and it prevents multiple variables from trying to free the same memory.
fn main() {
let v1 = vec![1, 2, 3];
let v2 = v1; // Ownership moves from v1 to v2
// v1 is no longer valid
// println!("v1: {:?}", v1); // Error: value used after move
}
Borrowing Rules
Borrowing allows you to reference data without taking ownership. In Rust, you can have:
- One mutable reference (
&mut T
) - OR any number of immutable references (
&T
) - References must always be valid (no dangling references)
fn main() {
let mut s = String::from("hello");
// Immutable borrow - multiple allowed
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// Mutable borrow - only one allowed and no immutable borrows can exist simultaneously
let r3 = &mut s;
r3.push_str(", world");
println!("{}", r3);
// r1 and r2 can't be used here because a mutable borrow exists
// println!("{}", r1); // Error: cannot borrow as immutable because it is also borrowed as mutable
}
Benefits
These rules provide several advantages:
- Memory Safety: Prevents data races and null pointer dereferences
- No Runtime Cost: All checks happen at compile time
- Concurrency Safety: Prevents data races in multithreaded code
- No Garbage Collection: Predictable performance without GC pauses
Mutations with immutable references: Interior Mutability
Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data. This appears to violate Rust’s borrowing rules, but it’s actually safely implemented using specific types that use runtime checks or unsafe code with guarantees to maintain memory safety.
This pattern is useful when:
- You need to modify data through a shared reference (when you have
&T
but need to change it) - You need to implement a logical “mutability” that differs from Rust’s default compile-time model
- You’re working with APIs that expect immutable references but need to track state changes internally
Rust provides several types in the standard library that implement interior mutability:
RefCell
Provides runtime-checked borrowing rules (versus compile-time checks) for single-threaded scenarios:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// We can borrow mutably even though data is not declared as mut
{
let mut value = data.borrow_mut();
*value += 1;
} // mut borrow ends here
// Now we can borrow immutably
println!("Value: {}", *data.borrow());
// If we try to borrow mutably when an immutable borrow exists, it will panic at runtime
// let value = data.borrow();
// let mut value2 = data.borrow_mut(); // Would panic!
}
Cell
Simpler than RefCell, allows getting and setting the whole value without borrowing. It only works for types that implement the Copy trait or when you’re replacing the entire value:
use std::cell::Cell;
fn main() {
let counter = Cell::new(0);
// No borrowing needed - the whole value is replaced
counter.set(counter.get() + 1);
println!("Counter: {}", counter.get());
// Example with non-Copy type using replace
let text = Cell::new(String::from("hello"));
let old_value = text.replace(String::from("world"));
println!("Old: {}, New: {}", old_value, text.take());
}
Mutex and RwLock
For interior mutability in multithreaded contexts - these are thread-safe alternatives to RefCell:
use std::sync::Mutex;
use std::thread;
use std::sync::Arc;
fn main() {
// Arc provides shared ownership across threads
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// Clone the Arc, not the Mutex
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Interior mutability comes with important tradeoffs:
- Some implementations (like RefCell) move borrow checking from compile time to runtime
- Can lead to runtime panics if borrowing rules are violated
- Adds a small performance overhead due to the runtime checks
- Thread-safe versions (Mutex, RwLock) can lead to deadlocks if used incorrectly
The key insight is that interior mutability doesn’t actually break Rust’s safety guarantees - it just shifts when and how they’re enforced. The standard library uses unsafe code internally to implement these types, but wraps it in safe interfaces that preserve Rust’s memory safety guarantees.
Copying
Some types in Rust are “Copy” types, meaning when you assign a value, the data is copied rather than moved. These are typically simple types stored on the stack:
fn main() {
let x = 5;
let y = x; // x is copied to y, both remain valid
println!("x: {}, y: {}", x, y); // Both x and y are usable
}
Types that implement the Copy
trait include:
- All integer types (i32, u64, etc.)
- Boolean type (bool)
- Floating point types (f32, f64)
- Character type (char)
- Tuples, if they only contain types that also implement Copy
Cloning
Cloning is an explicit deep copy operation for types that don’t implement Copy
. It’s used when you want to duplicate data that would otherwise be moved:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicitly creates a deep copy
// Both s1 and s2 are valid and independent
println!("s1: {}, s2: {}", s1, s2);
}
Clone-on-Write (Cow)
Cow
(Clone-on-Write) is a smart pointer in Rust’s standard library that allows for efficient handling of both borrowed and owned data. It’s particularly useful when you might need to modify data that you usually only read.
use std::borrow::Cow;
fn main() {
// Start with borrowed data
let borrowed: Cow<str> = Cow::Borrowed("hello");
println!("borrowed: {}", borrowed);
// When we need to modify, Cow will clone automatically
let owned: Cow<str> = Cow::Borrowed("hello");
let owned = owned.to_mut(); // Clones the data and returns a mutable reference
owned.push_str(", world");
println!("owned: {}", owned);
// Conditionally modify based on some logic
fn capitalize(s: &str) -> Cow<str> {
if s.chars().next().map_or(false, |c| c.is_uppercase()) {
// Already capitalized, return borrowed
Cow::Borrowed(s)
} else {
// Needs modification, return owned
let mut owned = String::from(s);
if let Some(first_char) = owned.get_mut(0..1) {
first_char.make_ascii_uppercase();
}
Cow::Owned(owned)
}
}
let s1 = "hello";
let s2 = "World";
println!("s1 capitalized: {}", capitalize(s1));
println!("s2 capitalized: {}", capitalize(s2));
}
Cow is useful when:
- You usually only read data (using borrowed references)
- Occasionally need to modify the data (creating owned copies)
- Want to avoid unnecessary cloning when no modifications are needed
This pattern optimizes for the common case of read-only access while still allowing modifications when necessary.
Drop trait
The Drop
trait is Rust’s destructor mechanism. It determines what happens when a value goes out of scope.
- It’s automatically called when a value goes out of scope
- It’s part of Rust’s Resource Acquisition Is Initialization (RAII) pattern
- It allows for deterministic resource cleanup
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data: {}", self.data);
// Clean up resources here
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
// c.drop(); // Error: explicit calls to drop not allowed
// To drop early, use std::mem::drop
// drop(c);
println!("End of main");
} // c goes out of scope here and drop() is called automatically
The Drop
trait is used extensively in Rust’s standard library for types like Box
, Rc
, Arc
, Mutex
, and more to ensure resources are properly cleaned up.
Lifetimes
Lifetimes are Rust’s way of describing how long references are valid. They’re denoted with an apostrophe followed by a name (e.g., 'a
).
Problems solved by lifetimes
- Dangling References: Prevents references to data that no longer exists
- Reference Validation: Ensures references are valid for as long as they’re used
- Data Structure Design: Allows defining data structures that contain references safely
Benefits of lifetimes
- Compile-Time Safety: All lifetime issues are caught at compile time
- Zero Runtime Cost: No performance penalty for lifetime checks
- API Documentation: Makes clear how long references need to live
// Function that takes two strings and returns the longer one
// 'a indicates the returned reference lives at least as long as the input references
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(&string1, &string2);
println!("The longest string is {}", result);
} // string2 goes out of scope here
// This would cause a compile error because string2 is gone
// let result = longest(&string1, &string2);
}
Common lifetime patterns
'static
: References that live for the entire program- Lifetime Elision: Rust’s rules for automatically inferring lifetimes
- Struct Lifetimes: When structs contain references
// A struct that contains a reference
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
}