Skip to content

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.