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 25 - Part 1
RUST

Top 25 - Part 1

Practice the first 15 questions from a curated set of the top 25 Rust programming interview questions.

1. What is the fundamental difference between Rc<T> and Arc<T>?

Answer: Rc<T> is for single-threaded use (non-atomic refcounting); Arc<T> uses atomic operations for thread-safe reference counting across threads.

Both provide shared ownership via a reference count:

  • Rc<T> — Reference Counted. Updates the counter with plain (non-atomic) increments/decrements. Fast, but not Send/Sync: if you share an Rc across threads, the counter can race → use only on a single thread.
  • Arc<T> — Atomically Reference Counted. Uses atomic operations to bump the count, safe across threads. Slightly slower than Rc (atomic ops have overhead) but thread-safe.
let rc = Rc::new(5);      // single-threaded only
let arc = Arc::new(5);    // shareable with std::thread::spawn

The value itself is still shared immutably in both — pair with Mutex/RefCell for interior mutability. The interview answer: Rc = non-atomic, single-threaded; Arc = atomic, multi-threaded.

2. What pattern is commonly paired with Rc<T> or Arc<T> to achieve “interior mutability”?

Answer: RefCell<T>/Cell<T> for single-threaded, or Mutex<T>/RwLock<T> for multi-threaded mutation.

Rc<T> and Arc<T> only expose immutable shared access to their contents. To mutate data behind a shared smart pointer, you need interior mutability — mutation through an & reference:

  • Single-threaded: Rc<RefCell<T>> or Rc<Cell<T>> (e.g., a shared graph node whose children can be modified).
  • Multi-threaded: Arc<Mutex<T>> or Arc<RwLock<T>> — the lock provides the exclusive access.
let shared = Rc::new(RefCell::new(0));
*shared.borrow_mut() += 1;      // mutate through shared ownership

let thread_safe = Arc::new(Mutex::new(0));
*thread_safe.lock().unwrap() += 1;

The pattern: shared pointer for ownership + a mutability mechanism for writes. The interview answer: RefCell/Cell (single-thread) or Mutex/RwLock (multi-thread) provide interior mutability inside Rc/Arc.

3. How does RefCell<T> enforce borrowing rules compared to standard Rust references?

Answer: RefCell<T> defers the borrow checks to runtime — a violated rule causes a panic, not a compile error.

Normal references (&T, &mut T) are checked by the borrow checker at compile time. RefCell<T> moves the same rules into the running program:

  • .borrow() — get an immutable borrow (multiple allowed, like &T).
  • .borrow_mut() — get a mutable borrow (only one allowed, like &mut T).
  • If you violate the rules at runtime — two simultaneous borrow_mut(), or a borrow_mut() while borrows are active — the program panics (“already mutably borrowed”).
let cell = RefCell::new(5);
let a = cell.borrow();
let b = cell.borrow_mut();   // panics: already borrowed

When to use: when the borrow checker is too strict for your pattern (self-referential data, caches, graph structures) but you still want the guarantees. Trade-off: compile-time safety becomes runtime panic risk. The interview answer: RefCell checks borrow rules at runtime and panics on violation, instead of rejecting at compile time.

4. What are Rust’s explicit lifetime annotations (e.g., ‘a) used for by the compiler?

Answer: They let the borrow checker verify references don’t outlive their data, describing relationships between references — with zero runtime cost.

Lifetimes are purely compile-time. 'a annotates a reference’s validity scope and, more importantly, relates multiple references:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The signature says: x and y both live at least as long as 'a, and the returned reference is valid as long as 'a too. The borrow checker uses this to guarantee the returned reference points at memory that’s still alive wherever the caller uses it — making dangling references impossible in safe Rust.

Key points:

  • Zero runtime cost — annotations vanish in the compiled code; they’re only for the compiler.
  • The compiler also elides lifetimes in common cases (you rarely write them).
  • They express relationships, not concrete durations.

The interview answer: lifetime annotations let the borrow checker prove references stay valid as long as they’re used; purely compile-time, no runtime overhead.

5. What will the following code produce?

fn main() {
    let mut x = 10;
    let y = &mut x;
    *y += 5;
    println!("{}", x);
}

Answer: Prints 15 — the mutable borrow ends before println!.

Under NLL (Non-Lexical Lifetimes), a borrow lives only until its last use, not until the end of the block. Here:

  1. y borrows x mutably; *y += 5 adds 5 → x becomes 15.
  2. After that line, y is never used again — the mutable borrow ends.
  3. println!("{}", x) then immutably borrows x — legal, since no mutable borrow is active.

Output: 15. (With the old lexical-lifetime rules, this would have been a compile error; NLL — stable since Rust 2018 — makes it work.) The interview answer: 15 — NLL ends the mutable borrow after its last use, so println! can read x.

6. What trait must a type implement to allow explicit cloning using .clone()?

Answer: The Clone trait.

Clone is the trait that provides .clone() — an explicit, potentially expensive duplication method:

#[derive(Clone)]
struct Point { x: i32, y: i32 }
let a = Point { x: 1, y: 2 };
let b = a.clone();   // deep copy
  • Explicit — cloning is a deliberate act (the compiler won’t clone for you).
  • Customizable — you implement Clone::clone to define what “copy” means (e.g., deep-cloning a String).
  • Types like String, Vec, Box, and everything Copy are Clone.

Contrast Copy (next question): implicit, bitwise duplication. A type can be Clone without being Copy (like String), but every Copy type must also be Clone. The interview answer: the Clone trait defines explicit .clone() duplication.

7. What is the structural difference between the Copy and Clone traits?

Answer: Copy is a marker trait for implicit bitwise duplication (memcpy); Clone is an explicit, method-driven duplication mechanism.

  • Copy: when you assign or pass a Copy value, the bits are simply copied — the original remains usable. It’s a marker (no methods to implement); it signals “cheap, safe to duplicate implicitly.” It can only be derived if all fields are Copy, and a type implementing Drop cannot be Copy.
  • Clone: provides an explicit .clone() method that can do arbitrary (possibly deep/expensive) duplication. Doesn’t imply implicit copying.
let a = 5;
let b = a;      // b is a bitwise copy; a still usable (i32 is Copy)

let s = String::from("hi");
let t = s;      // MOVES (String is not Copy)
let t2 = s.clone();  // explicit deep copy

Key rule: Copy requires Clone (a Copy type must also implement Clone), and Copy forbids Drop. The interview answer: Copy = implicit bitwise marker trait; Clone = explicit .clone() method trait, allowing non-trivial copies.

8. What issue is solved by using std::sync::Mutex<T> in concurrent Rust code?

Answer: Mutual exclusion — it guarantees only one thread accesses the guarded data at a time.

Mutex<T> wraps shared data in a lock:

  • .lock() blocks the calling thread until it acquires the lock, returning a MutexGuard.
  • While the guard is held, no other thread can access the data.
  • When the guard is dropped, the lock releases automatically (RAII) — even on early returns.
let counter = Arc::new(Mutex::new(0));
let mut guard = counter.lock().unwrap();
*guard += 1;   // guard dropped here → lock released

Notes: lock() returns Result — a poisoned mutex (a thread panicked while holding the lock) yields Err, which is why .unwrap()/.ok() handling is common. Mutex gives safe interior mutability across threads (unlike Rc<RefCell> which is single-threaded). The interview answer: mutual exclusion — one thread at a time via a lock guard that auto-releases on drop.

9. What is a “dangling reference” in Rust, and how does Rust prevent it?

Answer: A reference pointing at deallocated/out-of-scope memory; Rust’s borrow checker rejects such code at compile time.

A dangling reference is the classic C/C++ bug: a reference (or pointer) that outlives the data it points to — the data is freed or its scope ends, but the reference is still used. In Rust:

fn dangle() -> &String {        // compile error
    let s = String::from("hi");
    &s                          // s dies when the function returns
}

The borrow checker rejects this at compile time — it proves a reference can’t outlive its referent using lifetimes, so the error appears before the program ever runs. No dangling references are possible in safe Rust; unsafe code must uphold the rule manually. The interview answer: a reference to freed/out-of-scope memory; the borrow checker plus lifetime analysis prevents it at compile time.

10. What is the role of Send and Sync auto-traits in Rust concurrency?

Answer: Send = the type’s ownership can move across threads; Sync = it’s safe to share &T references across threads (T: Sync iff &T: Send).

  • Send: safe to transfer the value to another thread. Most types are SendString, Vec, primitives. Rc<T> is not Send (non-atomic refcount), Arc<T> is.
  • Sync: safe for multiple threads to hold &T simultaneously. &T: SendT: Sync. So Mutex<T> is Sync (the lock serializes access); Cell<T>/RefCell<T> are not Sync.

These are auto-traits: the compiler implements them automatically based on fields, and you can’t implement them manually for most types. They’re the mechanism behind the compiler rejecting code that shares Rc or RefCell across threads:

fn spawn() {
    let rc = Rc::new(1);
    std::thread::spawn(move || { /* use rc */ });   // error: Rc is not Send
}

The interview answer: Send = ownership transferable between threads; Sync = references shareable between threads; both checked at compile time.

11. What happens if you try to implement the Copy trait on a struct that contains a String field?

Answer: The compiler rejects itString doesn’t implement Copy, and Copy requires all fields to be Copy.

Copy demands that the type can be duplicated with a plain bitwise copy. String owns heap memory and implements Drop, so it can’t be Copy (a bitwise copy would double-free the buffer). Since a Copy type’s fields must themselves be Copy (and a Copy type can’t implement Drop), deriving/implementing Copy on a struct with a String is a compile error.

#[derive(Copy, Clone)]        // error: the trait `Copy` cannot be implemented
struct Bad { s: String }

The struct can still be Clone (a deep-copying .clone()), just not Copy. Rule of thumb: Copy is for plain bit-fields (ints, floats, bools, char, fixed arrays of those); anything owning resources is Clone-only. The interview answer: compile error — String isn’t Copy, and Copy can’t be applied to types with non-Copy fields (or Drop).

12. How do traits in Rust compare to interfaces in languages like Java or TypeScript?

Answer: Traits define shared behavior types can implement, enabling static or dynamic dispatch — they’re Rust’s interface/abstract-class analog.

A trait declares method signatures a type must provide:

trait Drawable { fn draw(&self); }
struct Circle;
impl Drawable for Circle { fn draw(&self) { /* ... */ } }

Differences/similarities vs Java interfaces:

  • Methods + default implementations — like interfaces (Java 8+) / abstract classes.
  • No data fields — traits define behavior, not state (like interfaces).
  • Static dispatch via generics: fn f<T: Drawable>(t: T) compiles to a concrete call (monomorphization, zero overhead).
  • Dynamic dispatch via trait objects: &dyn Drawable / Box<dyn Drawable> routes calls through a vtable at runtime.
  • Implementable for external types — you can implement a trait for a type you don’t own (or a type for a trait you don’t own, subject to the orphan rule).

The interview answer: traits are interface-like definitions of shared behavior, usable with static (generics) or dynamic (dyn Trait) dispatch.

13. What is “monomorphization” in Rust generics?

Answer: A compile-time process where the compiler generates concrete code per type used with a generic.

When you write a generic function:

fn max<T: Ord>(a: T, b: T) -> T { if a > b { a } else { b } }
max(1, 2);       // generates max_i32
max(1.5, 2.5);   // generates max_f64

The compiler “stamps out” a specialized copy for each concrete type instantiation. Benefits:

  • Zero-cost abstraction — no runtime dispatch or boxing; each specialization is fully optimized for its type (can inline, constant-fold).
  • Trade-off: binary bloat — more code for more instantiations.

This is what makes generic code as fast as hand-written per-type code. The interview answer: compile-time specialization of generics into per-type code copies — fast at runtime, larger binaries.

14. What is the difference between static dispatch and dynamic dispatch when using traits?

Answer: Static dispatch resolves calls at compile time (zero overhead); dynamic dispatch resolves calls at runtime via a vtable (dyn Trait).

  • Static dispatch — generic bounds: fn f<T: Drawable>(t: T). The compiler knows T at compile time, monomorphizes, and emits a direct call. No runtime cost. Downsides: each type gets its own code copy.
  • Dynamic dispatch — trait objects: fn f(t: &dyn Drawable). The reference carries a pointer to a vtable (method pointers) for the concrete type; calls are routed through the vtable at runtime. One code path for all types, but a small indirection cost. Requires the type to be sized-unsafe behind dyn (&dyn Trait, Box<dyn Trait>).
fn draw_static<T: Drawable>(t: T) { t.draw(); }        // compile-time
fn draw_dyn(t: &dyn Drawable) { t.draw(); }            // runtime vtable

The interview answer: static = compile-time call resolution via generics (zero overhead); dynamic = runtime vtable lookup via dyn Trait (small indirection).

15. What is the execution behavior of Rust’s match expressions?

Answer: Matches must be exhaustive — every possible value must be covered by a pattern arm, enforced by the compiler.

match is Rust’s pattern-matching powerhouse, and exhaustiveness is mandatory:

enum Coin { Penny, Nickel, Dime, Quarter }
fn value(c: Coin) -> u32 {
    match c {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,   // if this arm were missing → compile error
    }
}

If a pattern isn’t handled, the compiler rejects the code. This is a feature: adding a new enum variant forces you to update every match (the compiler tells you where). The catch-all _ => ... handles remaining cases explicitly. No fall-through (unlike C switch); each arm’s value is the expression’s result, and arms bind variables from the pattern. The interview answer: match is exhaustive — the compiler requires all variants/values be covered.

My Private Notes

Notes are auto-saved locally to this device.