Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Top 50 - Part 2
RUST

Top 50 - Part 2

Practice the middle 15 questions from a comprehensive set of 50 important Rust programming interview questions.

1. What will be the output of this pattern matching expression?

fn main() {
    let x = Some(5);
    if let Some(5) = x {
        println!("five");
    } else {
        println!("other");
    }
}

Output: five.

if let matches the scrutinee against the pattern. x is Some(5), and the pattern Some(5) matches exactly (value 5). So the if branch runs, printing five.

(if let is the ergonomic form of match for single-pattern tests: match x { Some(5) => ..., _ => ... }.) The interview answer: fiveSome(5) matches the pattern Some(5).

2. What is the role of Cargo.lock in a Rust project?

Answer: It records the exact resolved versions and checksums of all dependencies, ensuring reproducible builds.

  • Cargo.toml declares dependency requirements (version ranges like "1.2.3" or "^1.0").
  • Cargo.lock pins the exact versions that were resolved — so everyone (and every CI run) builds with identical dependency versions, eliminating “works on my machine.”
# Cargo.lock snippet
[[package]]
name = "serde"
version = "1.0.200"        # exact, frozen

Behavior nuances:

  • Binaries: commit Cargo.lock — reproducibility matters.
  • Libraries: typically don’t commit it — downstream consumers resolve their own (crates are compiled in dependency-context). Cargo still uses it locally if present.

The interview answer: Cargo.lock pins exact dependency versions + checksums for reproducible, deterministic builds.

3. What does the pub(crate) visibility modifier mean?

Answer: The item is visible everywhere inside the current crate, but hidden from external crates.

pub(crate) is a scoped publicity:

pub(crate) fn helper() { /* ... */ }   // usable across the crate, not exported
  • Items with no modifier are private to their module.
  • Items with pub are exported to all downstream crates.
  • pub(crate) sits in between: public within the crate (any module can use it), but not part of the public API external crates can import.

Use it for internal APIs shared between modules that shouldn’t leak into the public interface. The interview answer: visible crate-wide, but not exported to external crates.

4. What is the difference between const and static items in Rust?

Answer: const is inlined at compile time (no address); static is a fixed memory location shared program-wide.

const MAX: u32 = 100;              // value inlined wherever used
static APP_NAME: &str = "app";     // one fixed address in the binary
static mut COUNTER: i32 = 0;       // mutable static (requires unsafe to touch)
  • const: a compile-time constant. Every use is replaced by its value — no storage of its own. Always immutable. Only constant expressions.
  • static: a true global variable at a fixed memory address, existing for the program’s lifetime. Can be mut (but accessing a mutable static requires unsafe and risks races) or Sync/Send-typed.

When to use which: const for constants/limits; static when you need a single address, a global singleton, or 'static-lifetime data. The interview answer: const = inlined compile-time value; static = fixed-address global storage shared across the program.

5. What happens when an arithmetic operation overflows in debug mode versus release mode?

Answer: Debug panics at runtime; release wraps (two’s complement) silently.

  • Debug builds: overflow checks are on. let x = 255u8 + 1; panics with “attempt to add with overflow” — catching the bug early.
  • Release builds (--release): checks are disabled for performance; the operation wraps around using two’s complement (255 + 1 wraps to 0).
let a: u8 = 255 + 1;     // debug: panic   |   release: 0

If you want deterministic behavior regardless of build, use explicit methods: wrapping_add, checked_add (returns Option), saturating_add, or overflowing_add. The interview answer: debug panics on overflow; release wraps silently — use checked_/wrapping_/saturating_ methods for explicit control.

6. What is the primary purpose of the std::borrow::Cow (Clone-on-Write) smart pointer?

Answer: It avoids unnecessary clones by borrowing data read-only, then cloning lazily only when mutation is requested.

Cow<'a, B> is an enum: Borrowed(&'a B) or Owned(B::Owned). It lets a function take either borrowed or owned data and only pay for a copy if the data actually gets modified:

fn process(input: &str) -> Cow<str> {
    if input.starts_with("prefix") {
        Cow::Borrowed(input)      // no allocation
    } else {
        Cow::Owned(format!("prefix{input}"))   // only now allocate
    }
}

Internally, .to_mut()/into_owned() triggers the clone when mutation is needed; read-only access (&*cow) uses the borrowed data directly. Use case: functions that may need to modify a string/vec/slice but want to avoid copying when they don’t. The interview answer: Cow borrows until mutation is requested, then clones on demand — avoiding needless allocations.

7. What is the default function parameter dispatch mechanism in Rust generics?

Answer: Static dispatch via compile-time monomorphization.

Generic functions default to static dispatch:

fn process<T: Trait>(item: T) { item.method(); }

At compile time, the compiler specializes the function for each concrete T and emits a direct call — no vtable, no runtime indirection. This is Rust’s default because it’s the fastest option.

Dynamic dispatch is the explicit opt-in, via trait objects (&dyn Trait, Box<dyn Trait>) — used when you need type erasure / a single code path. The interview answer: static dispatch by default, via compile-time monomorphization of generic code.

8. How do you declare a trait object for dynamic dispatch in modern Rust?

Answer: &dyn Trait or Box<dyn Trait> (or Rc<dyn Trait>, &mut dyn Trait, etc.).

The dyn keyword marks a trait object:

fn draw(s: &dyn Drawable) { s.draw(); }
let boxed: Box<dyn Drawable> = Box::new(Circle);
  • &dyn Trait — a fat pointer: data pointer + vtable pointer, so method calls resolve at runtime.
  • Box<dyn Trait> — same, but owns the object (heap).
  • &dyn and dyn are required in modern Rust (edition 2018+); bare &Trait is the old (deprecated) syntax.

Trait objects enable type erasure: one type can hold many concrete types that all implement the trait — at the cost of a small runtime dispatch overhead. The interview answer: &dyn Trait or Box<dyn Trait> — the dyn keyword signals runtime (vtable) dispatch.

9. What does impl Trait as a return type signify in Rust function signatures?

Answer: The function returns a single concrete type implementing the trait, without writing the (possibly complex) type name.

fn iter_pairs() -> impl Iterator<Item = (i32, i32)> { /* ... */ }
  • The caller knows the return type implements Iterator but not its exact name.
  • Static dispatch — no vtable; the concrete type is monomorphized.
  • The hidden concrete type must be the same across all return paths (you can’t return two different concrete types from different branches).
  • Unlike dyn Trait (runtime dispatch, type erasure, any implementor), impl Trait is a compile-time opaque type.

Use it to return closures/iterators without naming complex nested types, while keeping zero runtime overhead. The interview answer: returns a single opaque-but-concrete type implementing the trait via static dispatch; all return paths must share that concrete type.

10. What is the output of println!(”{}”, 10 / 4); in Rust?

Output: 2.

Both operands are integers, so this is integer division: the fractional part is truncated (toward zero for signed). 10 / 4 = 2.5 → truncated to 2. Output: 2.

To get 2.5, at least one operand must be floating point: 10.0 / 4 or 10 / 4.0. The interview answer: 2 — integer division truncates the remainder.

11. What does the #[inline] attribute suggest to the compiler?

Answer: It hints the compiler to replace calls to the function with the function body, eliminating call overhead.

#[inline] is a suggestion (not a command) that the function be inlined at call sites:

  • Pros: removes call/return overhead, enables further optimization across the call boundary.
  • Cons: grows binary size if inlined in many places.

Why it exists: without it, the compiler may refuse to inline across crate boundaries (public functions in one crate called from another), because the body isn’t visible. #[inline] (or #[inline(always)] for a stronger hint) makes the function’s body available for inlining even cross-crate. Used for small, hot functions (accessors, hot loop helpers). The interview answer: a hint to inline the function body at call sites, reducing call overhead at the cost of binary size.

12. What is the standard channel type in std::sync::mpsc?

Answer: Multi-Producer, Single-Consumer — many senders, one receiver.

mpsc = Multi-Producer, Single-Consumer:

let (tx, rx) = mpsc::channel::<String>();
let tx2 = tx.clone();                 // multiple senders
std::thread::spawn(move || tx.send("a".into()));
std::thread::spawn(move || tx2.send("b".into()));
rx.recv();  rx.recv();                // one consumer receives all
  • Multiple senders: clone the Sender to get more (each clone can be moved to a different thread).
  • One receiver: only a single Receiver exists to consume messages.

If you need multiple consumers or broadcast semantics, you’d use crossbeam channels or an Arc<Mutex<Vec>>. The interview answer: multi-producer, single-consumer — many Senders (via clone) and one Receiver.

13. What happens when all Sender instances associated with a std::sync::mpsc::Receiver are dropped?

Answer: The channel closes, and receiving operations return an error (RecvError for recv(), None for try_recv()).

When every Sender is dropped, no more messages can arrive — the channel is disconnected. The Receiver’s recv() then:

  • Drains any remaining buffered messages first.
  • Then returns Err(RecvError) (or try_recv()Err(TryRecvError::Disconnected), iter() ends).
let (tx, rx) = mpsc::channel();
drop(tx);
let r = rx.recv();   // Err(RecvError) — channel closed

This is the standard way a receiver knows senders are done (like EOF on a stream) — crucial for worker-pool shutdown patterns. The interview answer: the channel is closed and recv() returns Err(RecvError) (or try_recv returns Disconnected/None).

14. What is the role of the phantom type marker std::marker::PhantomData<T>?

Answer: It tells the compiler the struct logically owns or references T for type/lifetime/drop-check purposes, while occupying zero bytes.

PhantomData<T> is a zero-sized type used when a generic type parameter doesn’t appear in the struct’s fields:

struct Id<T> { value: u64, marker: PhantomData<T> }

Without the marker, T would be unused (error). With it, the compiler treats the struct as if it holds T, affecting:

  • Variance — how the type behaves with lifetimes (covariant vs invariant).
  • Drop check — whether the struct owns a T that must be dropped before something else.
  • Auto-traitsSend/Sync/Unpin inference based on T.

For example, raw-pointer wrappers and type-safe ID/tag types use PhantomData to encode type relationships in the type system without any runtime cost. The interview answer: a zero-sized marker that makes the compiler treat the struct as owning/referencing T for variance, drop-check, and auto-trait analysis.

15. Which macro is used to create custom formatted strings without printing them to standard output?

Answer: format!.

format! uses the same format-specifier machinery as println! but returns an owned String instead of printing:

let name = "world";
let s = format!("Hello, {name}!");   // s == "Hello, world!"

The family: print!/println! → stdout; eprint!/eprintln! → stderr; write!/writeln! → any Write destination (file, buffer); format! → a String. (sprintf! doesn’t exist in Rust — it’s C.) The interview answer: format! — it formats into a String without printing.

My Private Notes

Notes are auto-saved locally to this device.