The decision tree
When the compiler complains about ownership, walk through these questions in order:
Can the data flow be simpler?
Often the cleanest fix is rethinking the data flow. If two functions need to read the same data, pass references to both.
Borrow it
If the data outlives the place you need it, borrow it.
fn process(data: &[u32]) -> u32 {
data.iter().sum()
}
let values = vec![1, 2, 3];
let total = process(&values);
// values still valid hereUse & for read access and &mut for write access. You can have many shared references or one mutable reference at a time.
Move it
If the caller doesn't need it anymore, move it.
fn consume(data: Vec<u32>) {
// data is ours now
}
let values = vec![1, 2, 3];
consume(values);
// values is GONE, can't use itClone it
If both places need their own copy and the data is small, clone it.
let a = vec![1, 2, 3];
let b = a.clone();
// both a and b are valid, independent copiesAvoid cloning large data in hot paths. Cloning a Vec<u8> with 10MB of data copies 10MB. Cloning a 20-character String is usually fine. Let measurement guide the cleanup.
When to use smart pointers
| Type | When | Overhead |
|---|---|---|
Box<T> | Single owner, heap allocated | 1 pointer, no runtime cost |
Rc<T> | Multiple owners, single thread | Reference count (not atomic) |
Arc<T> | Multiple owners, multi thread | Atomic reference count |
Cow<'a, T> | Maybe borrowed, maybe owned | Enum tag + data |
Box
Use Box when you need a fixed-size type to hold a dynamically-sized thing, or when you have a recursive type.
enum Tree {
Leaf(i32),
Node(Box<Tree>, Box<Tree>),
}Box gives the recursive branch a known pointer size, which lets the compiler compute the size of Tree.
Rc and Arc
Use Rc when multiple parts of your program need to read the same data and you can't figure out who should own it. Use Arc when those parts are on different threads.
use std::sync::Arc;
let config = Arc::new(load_config());
let config_clone = Arc::clone(&config);
std::thread::spawn(move || {
// config_clone is valid here
println!("{}", config_clone.database_url);
});Arc::clone doesn't copy the data. It increments the reference count. The data is freed when the last Arc is dropped.
Cow
Use Cow when data is usually borrowed and occasionally needs an owned modified copy. It borrows when possible and clones on mutation.
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<str> {
if input.contains('\t') {
Cow::Owned(input.replace('\t', " "))
} else {
Cow::Borrowed(input)
}
}If the input has no tabs, zero allocation. If it does, one allocation. Perfect for functions that usually pass data through unchanged.
Interior mutability
When shared data needs controlled mutation:
| Type | Thread safe | Panics on misuse | Cost |
|---|---|---|---|
Cell<T> | No | No (Copy types only) | Zero |
RefCell<T> | No | Yes, at runtime | Borrow flag |
Mutex<T> | Yes | No (blocks) | Lock overhead |
RwLock<T> | Yes | No (blocks) | Lock overhead |
AtomicU64 etc | Yes | No | Atomic ops |
RefCell is the most common choice for single-threaded interior mutability. It moves Rust's borrow checking from compile time to runtime. Two overlapping mutable borrows cause a panic.
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4);
println!("{:?}", data.borrow());Prototyping with clones
When prototyping, clone freely. Get the logic right first, then remove unnecessary clones when you profile. The compiler is good at optimizing away unnecessary clones, and the ones that survive are usually cheap enough to leave alone.
Hot loops processing millions of items deserve more care because every allocation counts. Treat that as optimization work after the shape of the code is clear.
