6.6 KiB
Thinking in Rust: Mental Models
Core Mental Models
1. Ownership as Resource Management
Traditional: "Who has a pointer to this data?"
Rust: "Who OWNS this data and is responsible for freeing it?"
Key insight: Every value has exactly one owner. When the owner goes out of scope, the value is dropped.
{
let s = String::from("hello"); // s owns the String
// use s...
} // s goes out of scope, String is dropped (memory freed)
2. Borrowing as Temporary Access
Traditional: "I'll just read from this pointer"
Rust: "I'm borrowing this value, owner still responsible for it"
Key insight: Borrows are like library books - you can read them, but must return them.
fn print_length(s: &String) { // borrows s
println!("{}", s.len());
} // borrow ends, caller still owns s
let my_string = String::from("hello");
print_length(&my_string); // lend to function
println!("{}", my_string); // still have it
3. Lifetimes as Validity Scopes
Traditional: "Hope this pointer is still valid"
Rust: "Compiler tracks exactly how long references are valid"
Key insight: A reference can't outlive the data it points to.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 'a means: the returned reference is valid as long as BOTH inputs are valid
if x.len() > y.len() { x } else { y }
}
Shifting Perspectives
From "Everything is a Reference" (Java/C#)
Java mental model:
// Everything is implicitly a reference
User user = new User("Alice"); // user is a reference
List<User> users = new ArrayList<>();
users.add(user); // shares the reference
user.setName("Bob"); // affects the list too!
Rust mental model:
// Values are owned, sharing is explicit
let user = User::new("Alice"); // user is owned
let mut users = vec![];
users.push(user); // user moved into vec, can't use user anymore
// user.set_name("Bob"); // ERROR: user was moved
// If you need sharing:
use std::rc::Rc;
let user = Rc::new(User::new("Alice"));
let user2 = Rc::clone(&user); // explicit shared ownership
From "Manual Memory Management" (C/C++)
C mental model:
char* s = malloc(100);
// ... must remember to free(s) ...
// ... what if we return early? ...
// ... what if an exception occurs? ...
free(s);
Rust mental model:
let s = String::with_capacity(100);
// ... use s ...
// No need to free - Rust drops s automatically when scope ends
// Even with early returns, panics, or any control flow
From "Garbage Collection" (Go/Python)
GC mental model:
# Create objects, GC will figure it out
users = []
for name in names:
users.append(User(name))
# GC runs sometime later, when it feels like it
Rust mental model:
let users: Vec<User> = names
.iter()
.map(|name| User::new(name))
.collect();
// Memory is freed EXACTLY when users goes out of scope
// Deterministic, no GC pauses, no unpredictable memory usage
Key Questions to Ask
When Designing Functions
-
Does this function need to own the data, or just read it?
- Need to keep it: take ownership (
fn process(data: Vec<T>)) - Just reading: borrow (
fn process(data: &[T])) - Need to modify: mutable borrow (
fn process(data: &mut Vec<T>))
- Need to keep it: take ownership (
-
Does the return value contain references to inputs?
- Yes: need lifetime annotations
- No: lifetime elision usually works
When Designing Structs
-
Should this struct own its data or reference it?
- Long-lived, independent: own (
name: String) - Short-lived view: reference (
name: &'a str)
- Long-lived, independent: own (
-
Do multiple parts need to access the same data?
- Single-threaded:
Rc<T>orRc<RefCell<T>> - Multi-threaded:
Arc<T>orArc<Mutex<T>>
- Single-threaded:
When Hitting Borrow Checker Errors
-
Am I trying to use a value after moving it?
- Clone it, borrow it, or restructure the code
-
Am I trying to have multiple mutable references?
- Scope the mutations, use interior mutability, or redesign
-
Does a reference outlive its source?
- Return owned data instead, or use
'static
- Return owned data instead, or use
Common Patterns
The Clone Escape Hatch
When fighting the borrow checker, .clone() often works:
// Can't do this - double borrow
let mut map = HashMap::new();
for key in map.keys() {
map.insert(key.clone(), process(key)); // ERROR: map borrowed twice
}
// Clone to escape
let keys: Vec<_> = map.keys().cloned().collect();
for key in keys {
map.insert(key.clone(), process(&key)); // OK
}
But ask: "Is there a better design?" Often, restructuring is better than cloning.
The "Make It Own" Pattern
When lifetimes get complex, make the struct own its data:
// Complex: struct with references
struct Parser<'a> {
input: &'a str,
current: &'a str,
}
// Simpler: struct owns data
struct Parser {
input: String,
position: usize,
}
The "Split the Borrow" Pattern
struct Data {
field_a: Vec<i32>,
field_b: Vec<i32>,
}
// Can't borrow self mutably twice
fn process(&mut self) {
// for a in &self.field_a {
// self.field_b.push(*a); // ERROR
// }
// Split the borrow
let Data { field_a, field_b } = self;
for a in field_a.iter() {
field_b.push(*a); // OK: separate borrows
}
}
The Rust Way
Embrace the Type System
// Don't: stringly-typed
fn connect(host: &str, port: &str) { ... }
connect("8080", "localhost"); // oops, wrong order
// Do: strongly-typed
struct Host(String);
struct Port(u16);
fn connect(host: Host, port: Port) { ... }
// connect(Port(8080), Host("localhost".into())); // compile error!
Make Invalid States Unrepresentable
// Don't: runtime checks
struct Connection {
socket: Option<Socket>,
connected: bool,
}
// Do: types enforce states
enum Connection {
Disconnected,
Connected { socket: Socket },
}
Let the Compiler Guide You
// Start with what you want
fn process(data: ???) -> ???
// Let compiler errors tell you:
// - What types are needed
// - What lifetimes are needed
// - What bounds are needed
// The error messages are documentation!
Summary: The Rust Mental Model
- Values have owners - exactly one at a time
- Borrowing is lending - temporary access, owner retains responsibility
- Lifetimes are scopes - compiler tracks validity
- Types encode constraints - use them to prevent bugs
- The compiler is your friend - work with it, not against it
When stuck:
- Clone to make progress
- Restructure to own instead of borrow
- Ask: "What is the compiler trying to tell me?"