Skip to content
Languages

11 Rust Traits and Dispatch Interview Questions and Answers

This focused guide turns RecallDeck’s curated Rust Traits and Dispatch material into 11 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

12 min read11 detailed answersUpdated
What to remember

Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.

Question set

11 detailed answers

A recorded source review is available for 1 of 11 answers. Each reviewed answer lists its date and scope.

01

Static (impl Trait/generics) vs dynamic (dyn Trait) dispatch — what's the difference, and what is monomorphization?

Short answer: With static dispatch the compiler knows the concrete type at compile time and emits a direct call — it can be inlined, with zero overhead. With dynamic dispatch the concrete type is only known at runtime, and the call goes through a vtable behind a pointer. Monomorphization is generating a separate copy of the generic code for each concrete type.

In depth:

  1. Monomorphization — fn f<T: Trait>(x: T) called with u32 and String compiles to two specialized functions. Hence direct calls, inlining, and binary bloat.
  2. dyn Trait — one shared function hidden behind a vtable. The call is indirect, cross-call inlining is impossible, but the binary is smaller.
  3. Heterogeneity — Vec<Box<dyn Trait>> stores objects of different types in one collection; generics can't — Vec<T> holds elements of a single type.
Criterion Static (generics) Dynamic (dyn)
Resolution compile time runtime (vtable)
Call cost zero, inlinable indirect call
Binary size larger (copies) smaller
Heterogeneous types no yes

⚠️ Common mistake: treating dyn as "always slow." On a hot path the difference is usually within the noise; you pick it for flexibility and less code bloat, not just for performance.

02

How is dyn Trait laid out in memory, and how does a trait object differ from a C++ object with virtual methods?

Short answer: &dyn Trait and Box<dyn Trait> are a fat pointer of two words: a pointer to the data and a pointer to the vtable — 16 bytes on x86-64. In C++ the vtable pointer (vptr) lives inside the object itself; Rust keeps the data and the behavior table separate.

In depth:

  1. Fat pointer — (data_ptr, vtable_ptr). The data itself doesn't "know" its trait, so you can't rebuild a trait object from an erased pointer like *const () — there's no way to recover the vtable.
  2. What's in the vtable — pointers to the trait's methods, plus the size, alignment, and drop glue of the concrete type. That's how Box<dyn Trait> drops and deallocates correctly.
  3. Difference from C++ — there the vptr is embedded in the object, so the object grows by one word while a plain pointer stays thin. In Rust the object stays "clean" and the pointer widens instead.
&dyn Trait  (16 bytes on x86-64)
┌───────────┬────────────┐
│ data ptr  │ vtable ptr │
└─────┬─────┴──────┬─────┘
      │            ▼
      │      ┌──────────────────┐
      │      │ drop_glue        │
      │      │ size / align     │
      │      │ method_0 ...     │
      ▼      └──────────────────┘
  [ object data ]

⚠️ Common mistake: thinking Box<dyn Trait> is a single pointer. It's twice as wide as Box<T>, and inside Option/enums that noticeably affects size.

03

What is object safety? Why can't you have Box<dyn Clone>?

Short answer: Dyn compatibility determines whether a trait can form a trait object. A compatible trait may contain non-dispatchable methods: fn consume(self) is allowed, but cannot be called on an unsized dyn value. Clone itself is incompatible because it requires Sized; its method also returns Self.

In depth:

  • Dispatchable methods cannot have type parameters or use Self outside an allowed receiver type. A method returning Self can instead be excluded from dynamic dispatch with where Self: Sized.
  • A by-value self receiver implicitly has that restriction. Do not confuse “cannot call this method through the object” with “cannot construct the object.”
  • A trait requiring Self: Sized, associated constants, or generic associated types is not dyn-compatible. Generic methods and associated functions without a receiver can be excluded using a method-level where Self: Sized.
trait Consume {
    fn consume(self); // non-dispatchable; does not forbid dyn Consume
}
struct Item;
impl Consume for Item { fn consume(self) {} }
fn main() {
    let _item: Box<dyn Consume> = Box::new(Item); // compiles
}

Watch out: A clone_box(&self) -> Box<dyn Trait> method can support cloning boxed trait objects without returning unsized Self. It is a separate object-safe interface, not Box<dyn Clone>.

Sources: Rust dyn compatibility, Clone.

04

Why must dyn Trait always sit behind a pointer (Box, &, Rc), and what does ?Sized mean?

Short answer: dyn Trait is a DST (a type with no known size): different implementors have different sizes, so the compiler can't place it directly on the stack. Behind a pointer (&, Box, Rc) the size is known again — it's a fixed-width fat pointer. ?Sized relaxes the implicit Sized bound that every generic parameter gets by default.

In depth:

  1. Unsized type — dyn Trait and [T] have no statically known size, so they can't be a variable's value or an argument by value (in a struct — only the last field, which makes the struct itself a DST).
  2. A pointer pins the size — Box<dyn Trait> / &dyn Trait are two words wide and know the type via the vtable.
  3. Implicit Sized — fn f<T>(x: T) really means fn f<T: Sized>(x: T). To accept an unsized type you write T: ?Sized and take it by reference.
// accepts both sized types and dyn/slices behind a reference
fn print_it<T: std::fmt::Debug + ?Sized>(x: &T) {
    println!("{x:?}");
}

// let x: dyn Trait = ...;   // error: size not known
let x: Box<dyn std::fmt::Debug> = Box::new(42); // ok

⚠️ Common mistake: not knowing generics are Sized by default. That's why people are surprised T won't accept str or dyn Trait until they add ?Sized.

05

Associated type vs generic trait parameter: Iterator::Item vs From<T> — when do you choose which?

Short answer: Use an associated type when a concrete type has exactly one implementation of the trait (an iterator has one Item). Use a generic parameter when one type implements the trait many times, distinguishing impls by the parameter (From<u32>, From<String>…). A one-to-one relationship → associated type; one-to-many → parameter.

In depth:

  1. Associated type — Iterator with type Item can be implemented for a type only once, so x.next() needs no type annotation.
  2. Generic parameter — From<T> can be implemented any number of times: i64: From<i32>, i64: From<i16>, and so on.
  3. Cost of getting it wrong — pick a parameter for a unique relationship and the compiler needs turbofish everywhere (a collect::<Vec<_>>()-style ambiguity); the API turns noisy.
trait Iterator {
    type Item;                 // exactly one per type
    fn next(&mut self) -> Option<Self::Item>;
}

trait From<T> {               // many impls per type
    fn from(value: T) -> Self;
}

⚠️ Common mistake: making Iterator<Item> generic. Then one type could "iterate" into several different Items, type inference breaks, and you annotate everywhere.

06

What is the orphan rule and how does the newtype pattern work around it?

Short answer: The orphan rule lets you implement a trait for a type only if either the trait or the type is defined in your crate. It's part of coherence — the guarantee that no two conflicting impls appear across the ecosystem. The workaround is the newtype: wrap the foreign type in your own struct struct MyVec(Vec<T>) and implement the trait on the wrapper.

In depth:

  1. Why the rule — without it, crates A and B could each write impl Display for Vec<T>, and linking both would be ambiguous. Coherence forbids that.
  2. What's forbidden — implementing a foreign trait for a foreign type: impl Display for Vec<T> outside std won't compile.
  3. Newtype — a local wrapper makes the type "yours," satisfying the rule. #[repr(transparent)] guarantees the same memory layout — the wrapper is free.
use std::fmt;

#[repr(transparent)]
struct Wrapper(Vec<String>); // the type is now local

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

⚠️ Common mistake: trying impl ExternalTrait for ExternalType and hitting E0117. Mentioning #[repr(transparent)] for a zero-cost wrapper signals a strong candidate.

07

How does impl Trait in argument position differ from impl Trait in return position?

Short answer: impl Trait in an argument is sugar for an anonymous generic parameter: the function is monomorphized per concrete argument type. In return position (RPIT) it's one opaque concrete type, the same on every code path; you can't conditionally return two different types.

In depth:

  1. Argument — fn f(x: impl Trait) equals fn f<T: Trait>(x: T). The caller picks the type; static dispatch.
  2. Return (RPIT) — fn g() -> impl Trait hides the concrete type (e.g. a closure or iterator), but there's exactly one. The compiler infers it from the body.
  3. Branch restriction — if if/else branches return different concrete types, RPIT won't compile; you need Box<dyn Trait> or an enum.
fn make() -> impl Iterator<Item = i32> {
    if cond() {
        (0..10).filter(|x| x % 2 == 0)
    } else {
        (0..5).map(|x| x * 2)   // ERROR: a different concrete type
    }
}
// Fix: -> Box<dyn Iterator<Item = i32>> and Box::new(...) in each branch

⚠️ Common mistake: thinking -> impl Trait acts like dyn and allows different types on different paths. It's one static type; for runtime polymorphism you need dyn or an enum.

08

What bounds does derive(Clone) silently add to generic parameters, and when does that bite?

Short answer: #[derive(Clone)] puts T: Clone on every generic parameter of the struct, even when it isn't needed. The classic example is struct Wrapper<T>(Arc<T>): Arc<T> clones for any T, but the derive still demands T: Clone. The fix is a hand-written impl with tighter bounds.

In depth:

  1. What the derive does — it generates impl<T: Clone> Clone for Wrapper<T>, mechanically adding T: Clone across all parameters.
  2. Why it bites — Arc<T>: Clone doesn't depend on T (the pointer is cloned, not the value), so the extra bound narrows the API for no reason.
  3. The fix — write the impl by hand with the real bounds; then Wrapper<T> clones for any T.
use std::sync::Arc;

// Problem: requires T: Clone needlessly
#[derive(Clone)]
struct Wrapper<T>(Arc<T>);

// Fix: clones for ANY T
struct Wrapper2<T>(Arc<T>);
impl<T> Clone for Wrapper2<T> {
    fn clone(&self) -> Self { Wrapper2(Arc::clone(&self.0)) }
}

⚠️ Common mistake: assuming #[derive(Clone)] is free for generics. For wrappers over Arc/Rc/PhantomData it forces unnecessary bounds — a known derive-macro wart (the "perfect derive" problem).

09

How do Deref, AsRef and Borrow differ? Why does HashMap<String, V>::get accept &str?

Short answer: Deref powers auto-deref and coercions (&String → &str implicitly). AsRef is a cheap explicit conversion .as_ref() with no semantic guarantees. Borrow additionally promises that Hash/Eq/Ord agree between the owned and borrowed forms. HashMap::get relies on exactly this: String: Borrow<str>, and the hash of a &str equals the hash of the corresponding String.

In depth:

  1. Deref — about how a type "behaves like" another; enables deref coercion and auto-deref via */..
  2. AsRef<T> — a cheap explicit reference-to-reference conversion; guarantees nothing about hashing or equality.
  3. Borrow<T> — like AsRef but with a contract: the borrowed value hashes and compares identically to the owned one. Hence the signature fn get<Q>(&self, k: &Q) where K: Borrow<Q>.
Trait Guarantee Typical use
Deref "behaves like T" Box<T>, String→str
AsRef cheap conversion flexible function arguments
Borrow Hash/Eq/Ord agree HashMap/BTreeMap keys

⚠️ Common mistake: implementing Borrow where Hash/Eq diverge from the owned type. The contract is broken — and HashMap lookups silently fail to find existing keys.

10

Explain the Fn / FnMut / FnOnce closure hierarchy. What does the move keyword actually change?

Short answer: Which trait a closure implements is decided by how its body uses the captured variables: reads only → Fn, mutates → FnMut, consumes (moves them out) → FnOnce. The traits nest: Fn: FnMut: FnOnce. move only changes the capture mode — to by-value — not the trait choice.

In depth:

  1. FnOnce — callable once, because the body takes captures by value. The supertrait of the rest.
  2. FnMut — callable repeatedly while mutating state (&mut to the captures).
  3. Fn — callable repeatedly reading only (& to the captures). The "strongest," usable anywhere FnMut/FnOnce is expected.
  4. move — forces by-value capture (needed for threads and 'static closures), but move || println!("{x}") that just reads x is still Fn.
What the body does with captures Trait
reads only (&) Fn
mutates (&mut) FnMut
consumes (by-value) FnOnce

⚠️ Common mistake: "move makes the closure FnOnce." Wrong: the trait comes from how captures are used, not from move. A move closure that only reads a Copy value is Fn.

11

Rust has no class inheritance — how do traits and composition replace classic OOP?

Short answer: Traits give interface-style polymorphism — with default methods and blanket impls. A supertrait is an obligation to "also implement that trait," not inheritance of fields and behavior. Code reuse in Rust is built on composition: you embed a type in a struct and delegate (including via Deref).

In depth:

  1. Polymorphism — a shared interface via trait + default methods; statically (generics) or dynamically (dyn).
  2. Blanket impl — impl<T: Display> ToString for T gives ToString to every Display type at once — reuse without a hierarchy.
  3. Supertrait ≠ inheritance — trait B: A requires implementing A too, but doesn't inherit its state; it's a constraint, not "is-a."
  4. Composition — component fields + method delegation replace class chains; "has-a" instead of "is-a."
OOP concept Rust counterpart
interface trait
abstract method trait method with no body
default method default trait method
behavior inheritance composition + delegation
interface inheritance supertrait (trait B: A)

⚠️ Common mistake: calling a supertrait "inheritance." It drags along no fields or implementation — it's just a requirement to implement another trait.

Source notes

References and review policy

Source links provide further reading. Where a recorded technical review is available, the answer shows its date and scope. Tool selections use direct provider links and contain no affiliate placements. Sources, features, and hiring formats can change.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

Languages105 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers