Rust
What experienced Rust engineers forget before an interview, grouped by subtopic.
Ownership and borrowing
Move semantics and Copy
- A non-
Copy value has one owner; assignment or passing moves it, invalidating the old binding.
Copy is an implicit bitwise copy, allowed only if every field is Copy and there's no Drop; Clone is explicit and possibly expensive.
Borrow rules and NLL
- Many
&T or one &mut T at a time — never both for overlapping uses (borrowing rules).
- Non-lexical lifetimes: a borrow ends at its last use, not at the end of the block.
- Reborrowing (
&mut *r, implicit when passing r) makes a shorter borrow, so you can call several &mut self methods in a row.
Interior mutability
| Type |
Checked |
Thread-safe |
Note |
Cell<T> |
none needed |
no |
copy/replace in and out; no references through &Cell |
RefCell<T> |
runtime |
no |
a conflicting borrow panics |
OnceCell/OnceLock |
write once |
OnceLock yes |
lazy initialization |
Mutex/RwLock |
runtime (lock) |
yes |
can be poisoned |
Lifetimes
Elision rules
- Rule 1: each elided input reference gets its own lifetime.
- Rule 2: exactly one input lifetime → it's used for all outputs.
- Rule 3: in methods,
&self's lifetime goes to all outputs.
'static and HRTBs
T: 'static means "holds no non-static borrows" — String, Vec<u8> satisfy it, Box<&'a str> doesn't; it doesn't mean "lives forever". &'static T is the forever reference.
- HRTB (
for<'a> Fn(&'a T)) says "works for every lifetime", needed when the reference is only created inside the callee.
"Does not live long enough"
- Usually a temporary dropped at the end of the statement while still borrowed — bind it to a variable first.
- Or a struct outliving a reference it holds — find where the referent is actually owned, or store owned data instead.
Traits and generics
Static vs dynamic dispatch
- Generics are monomorphized: one copy per type, zero-cost calls, bigger binary.
dyn Trait is unsized; &dyn/Box<dyn> is a fat pointer (data + vtable): one copy, an indirect call, heterogeneous collections.
- Dyn compatibility (formerly "object safety"): roughly, no methods returning
Self by value and no generic methods.
Coherence: orphan rule and blanket impls
- Orphan rule: implement a trait only if your crate owns the trait or the type — prevents conflicting impls across crates.
- Blanket impls (
impl<T: Display> MyTrait for T) cover every type meeting a bound.
Associated types vs type parameters
- Associated types (
Iterator::Item) fix one type per impl; type parameters allow many impls for one type (From<A>, From<B>).
- GATs (1.65) let an associated type be generic, e.g. over a lifetime — the basis of lending iterators.
Sized and impl Trait
- Every type parameter is implicitly
Sized; ?Sized accepts str, [T], or dyn Trait behind a pointer.
impl Trait in argument position is an anonymous generic; in return position it hides the concrete type but stays static dispatch.
Eq, Ord, and Hash
PartialEq/PartialOrd allow incomparable values (NaN ≠ NaN); Eq promises an equivalence relation (a == a), Ord a total order.
f64 implements none of Eq, Ord, or Hash, so it can't key a HashMap or BTreeMap or use .sort(); use sort_by(f64::total_cmp) or an ordered wrapper.
Hash must agree with Eq (a == b ⇒ equal hashes), or map lookups silently fail.
- Derived
PartialOrd/Ord compare fields in declaration order; enums by discriminant (variant order unless set explicitly), then fields.
Async functions in traits
Closures, iterators, strings
Fn, FnMut, FnOnce
| Trait |
Captured state |
Callable |
Fn |
borrowed immutably |
many times, via &self (concurrently only if Sync) |
FnMut |
borrowed mutably |
many times, via &mut self |
FnOnce |
moved out / consumed |
once |
- Every
Fn is also FnMut and FnOnce; take the loosest bound your code allows (FnOnce if you call it once), so callers can pass more kinds of closures.
move changes how variables are captured (by value), not which trait is implemented — a move closure that only reads is still Fn.
- Async closures (
async || { ... }, 1.85) can borrow from their captures across .await, which || async { ... } couldn't; bound them with AsyncFn/AsyncFnMut/AsyncFnOnce.
Iterators
- Iterator adapters (
map, filter, …) are lazy: nothing runs until a consumer (collect, sum, for).
- They compile to the same code as a hand-written loop — a zero-cost abstraction.
String vs &str
Smart pointers and memory
Box, Rc, Arc, Cow
Box<T>: single owner on the heap. Rc<T>: shared, non-atomic count, one thread. Arc<T>: atomic count, cross-thread.
- An
Rc/Arc cycle leaks; break it with rc::Weak or sync::Weak.
Cow<T> borrows until a mutation forces an owned copy.
RAII, drop order, leaking
Deref coercion, AsRef, Borrow
- Deref coercion converts
&String → &str, &Vec<T> → &[T], &Box<T> → &T automatically at calls and method lookups.
AsRef<T> is a cheap reference conversion for flexible parameters: fn open<P: AsRef<Path>>(p: P).
Borrow<T> also promises identical Eq/Hash/Ord, which is why HashMap<String, V>::get accepts a &str.
Layout and niche optimization
- Niche optimization:
Option<&T>, Option<Box<T>>, and Option<NonZeroU32> are the same size as the inner type — the forbidden null or zero value encodes None.
- The default
repr(Rust) may reorder fields to cut padding; repr(C) fixes C field order for FFI.
&str, &[T], and &dyn Trait are two words (pointer + length or vtable); an enum is its largest variant plus a tag, unless a niche absorbs the tag.
Pin and Unpin
Pin<P> stops the pointee from moving only if it's !Unpin; most types are Unpin and move freely.
- Async state machines are
!Unpin because they can be self-referential — that's why futures are polled through Pin<&mut Self>.
Error handling
? and error crates
? returns early with Err, converting via From; on Option it returns None.
thiserror for libraries (typed enums callers can match); anyhow for applications (one dynamic error with context).
Integer overflow and casts
Panics and unwinding
panic! unwinds by default, running destructors; panic = "abort" exits immediately — smaller binary, no recovery.
catch_unwind stops an unwinding panic at a boundary (FFI, worker pool) — never for control flow, and useless under abort.
Concurrency
Send and Sync
Send: ownership can move to another thread. Sync: &T can be shared (T: Sync ⇔ &T: Send).
Rc is neither; RefCell<T> is Send if T: Send, never Sync; MutexGuard is not Send — it must be released on the locking thread.
Mutex poisoning and scoped threads
- A panic while holding a
Mutex poisons it; later lock() calls return Err so callers can decide whether the data is still valid.
thread::scope (1.63) lets threads borrow local data without 'static or Arc, since they must finish before the scope ends.
Channels
std::sync::mpsc: multi-producer single-consumer; channel() is unbounded, sync_channel(n) bounded.
- crossbeam adds multi-consumer channels and
select!.
Atomics and memory ordering
Relaxed: atomicity only. Release store + an Acquire load that reads its value: happens-before between the two threads. SeqCst: one global order, strongest and slowest.
- x86 is strongly ordered, so a too-weak ordering often works there and fails only on ARM.
Async
Futures and executors
- Futures are lazy — nothing runs until an executor (e.g. Tokio) polls them.
- Holding a non-
Send guard across .await makes the future non-Send, so it can't be spawned on a multi-threaded runtime.
tokio::spawn needs a Send + 'static future: move owned data or Arc clones in, not references.
- Tokio's default runtime is multi-threaded with work stealing;
current_thread plus a LocalSet runs non-Send futures; JoinSet spawns and awaits a group of tasks.
Blocking and locks in async code
- A blocking call inside async code stalls the worker thread and every task on it — move it to
spawn_blocking.
- A
std::sync::Mutex is fine in async code if it's never held across .await (and it's faster).
- Use
tokio::sync::Mutex only when the guard must live across an .await.
Cancellation is drop
- Dropping a future cancels it at its current
.await, with no signal and no chance to run async cleanup.
- Dropping a Tokio
JoinHandle detaches the spawned task instead — cancel with abort().
- Cancel safety: in
select!, a losing branch is dropped mid-await — read_line into a buffer can lose data, recv() on a channel can't.
Unsafe and FFI
What unsafe allows
- Five extra powers: dereference raw pointers, call unsafe functions, implement unsafe traits, access mutable statics, read union fields.
- It does not turn off the borrow checker for references.
Undefined behavior and Miri
- Common UB: two live
&mut to the same data, invalid values (a bool that isn't 0/1), data races on non-atomic memory.
- Miri interprets MIR and catches much UB that "works" in normal builds.
Macros and Cargo
Macro kinds and hygiene
macro_rules! pattern-matches token trees; procedural macros (derive, attribute, function-like) run code on a TokenStream at compile time.
- Hygiene:
macro_rules! locals and labels don't collide with the caller's (mixed-site); items and proc-macro output are unhygienic.
Features and workspaces
- Features are additive — unioned across the dependency graph — so a feature must never remove functionality.
- A workspace shares one
Cargo.lock and target/ across crates.
Lock files and editions
- Commit
Cargo.lock for binaries; since 2023, Cargo's guidance is to commit it for libraries too, as a CI baseline (dependents ignore it).
- Editions are opt-in, per-crate language changes; crates on different editions link together freely.
- Edition 2024 enables let chains (
if let Some(x) = a && x > 0, 1.88) and makes impl Trait in return position capture all in-scope lifetimes by default.