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

Top 50 - Part 1

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

1. What is the main purpose of the Deref trait coercion mechanism in Rust?

Answer: It lets smart pointers (like Box<T>, Rc<T>, or String) automatically behave like references to their underlying types — e.g., coercing &Box<String> to &str.

Deref coercion is implicit: when a function expects &str (or &[T]) and you pass a smart pointer whose Deref target matches, the compiler inserts the deref chains automatically:

fn takes_str(s: &str) {}
let b = Box::new(String::from("hi"));
takes_str(&b);   // &Box<String> → &String → &str, all implicit

String: Deref<Target = str>, Box<T>: Deref<Target = T>, Vec<T>: Deref<Target = [T]>, etc. This is why &*boxed, .methods() on smart pointers, and generic code over &T all “just work.”

Also note the deref method resolution: when you call a method on Box<String>, the compiler searches the deref chain for the method. The interview answer: Deref coercion implicitly converts &SmartPointer into &Target, letting smart pointers act like their inner types.

2. What guarantees does the Pin type provide in Rust async/await execution?

Answer: It guarantees the underlying object won’t be moved in memory, which is essential for self-referential structures like async futures.

Pin<P> pins a value to its memory address. Why it matters: an async function compiles into a future that can hold self-referential data — fields that contain pointers to other fields within the same struct. If the future were moved after construction, those internal pointers would dangle.

let fut = some_async_fn();     // may contain pointers into itself
pin!(fut);                     // now pinned — safe to poll

The compiler and runtime use Pin to guarantee a polled future never moves. Safe Rust can’t create a Pin<&mut T> from just any value precisely to prevent accidental moves of self-referential data; unsafe (or safe helpers like pin!/Box::pin) is needed when you know the value is safe to pin. The interview answer: Pin prevents the target from being moved, enabling self-referential async futures to remain valid.

3. What is an async function in Rust translated into by the compiler?

Answer: A state machine that implements the Future trait.

async fn foo() is desugared by the compiler into a hidden struct implementing Future:

  • The function body becomes a state machine — each .await point is a state; local variables are stored in the future’s fields (for use across awaits).
  • Calling the function returns a Future that does nothing until polled (futures are lazy).
  • Polling it drives the state machine forward until it resolves to a value.
async fn fetch() -> String { /* ... .await ... */ }
let fut: impl Future<Output = String> = fetch();   // lazy, not yet run

So async/await is not threads, callbacks, or kernel interrupts — it’s a compile-time state machine over the Future trait, polled by a runtime. The interview answer: the compiler translates async fn into a state machine implementing the Future trait.

4. What is required to make an async future actually execute and produce a result in Rust?

Answer: The future must be polled by an async runtime (Tokio, async-std, etc.) or awaited with .await.

Rust futures are lazy: constructing one does zero work. Execution happens only when an executor polls the future:

  • .await inside another async context hands the future to the surrounding runtime, which polls it to completion.
  • A standalone future needs a runtime driver (Tokio, async-std, smol, std) to poll it; without one, nothing runs.
#[tokio::main]
async fn main() {
    let result = fetch_data().await;   // the runtime polls fetch_data()
}

Without .await or an executor, the future is just an inert value that never executes. The interview answer: the future must be polled — via .await within a runtime such as Tokio — since futures perform no work until polled.

5. What is the output of the following expression?

fn main() {
    let v = vec![10, 20, 30];
    println!("{:?}", v.get(5));
}

Output: None.

.get(index) is the safe, non-panicking accessor: it returns Option<&T>. For an out-of-bounds index (5 is beyond the 3 elements), it returns None instead of panicking. Output: None.

Contrast: v[5] would panic (“index out of bounds”). .get() lets you handle the missing case gracefully. The interview answer: None.get() returns Option and doesn’t panic on out-of-bounds.

6. What is the key difference between vec[index] indexing and vec.get(index)?

Answer: Direct indexing panics on out-of-bounds; .get() safely returns None.

  • vec[i] — uses the Index trait; if i >= len, the program panics at runtime. Fast, no allocation, but can crash.
  • vec.get(i) — returns Option<&T>: Some(&value) if in bounds, None otherwise. No panic.
let v = vec![1, 2, 3];
v[5];        // panic: index out of bounds
v.get(5);    // None
v.get(1);    // Some(&2)

The [] operator can’t fail gracefully (it returns a value directly), so it panics. Use indexing when bounds are logically guaranteed; use .get() when the index might be invalid and you want to handle it. The interview answer: [] panics on out-of-bounds; .get() returns Option (Some/None) safely.

7. What is the function of the Drop trait in Rust?

Answer: It provides custom cleanup logic that runs automatically when a value goes out of scope.

Drop is Rust’s destructor trait:

impl Drop for MyResource {
    fn drop(&mut self) {
        // custom cleanup: close files, release handles, log, etc.
    }
}

The compiler calls drop automatically when the value’s scope ends (RAII) — you can’t call it manually (drop(x) is a stdlib function that consumes x, forcing the automatic drop). Because Rust has no garbage collector, this deterministic cleanup is how all resources (memory, files, locks) get released at a predictable point.

String, Vec, Box, File, etc. all implement Drop internally. Types implementing Drop can’t be Copy. The interview answer: Drop lets you define cleanup that runs automatically when a value leaves its scope.

8. Why can’t a type implement both the Copy trait and the Drop trait?

Answer: Copy silently duplicates via memcpy, which would create multiple owners of the same resource — each duplicate’s Drop would then double-free.

If a type were both Copy and Drop:

  1. Copy allows implicit bitwise duplication — let b = a; copies the bytes, leaving a and b sharing the same underlying resource (heap buffer, file handle).
  2. Both a and b go out of scope → both run drop → the same resource is freed twice.

Double-free is a classic memory-safety bug. The compiler therefore forbids Copy + Drop. Types that own resources (String, Vec) are Clone (explicit, deep-copying) but never Copy; only plain bit-copyable types (integers, bool, etc.) are Copy. The interview answer: Copy + Drop would double-free shared resources, so the compiler forbids the combination.

9. What does the #[derive(…)] attribute do in Rust?

Answer: It auto-generates implementations of specified traits for a struct or enum.

#[derive(Trait)] tells the compiler to synthesize a default implementation of the listed traits based on the type’s fields:

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User { id: u32, name: String }

Common derivable traits: Debug (formatting), Clone/Copy, PartialEq/Eq, PartialOrd/Ord, Hash, Default. It’s implemented via a procedural macro that inspects the type and generates the trait impl mechanically — each trait imposes constraints on the fields (e.g., Copy needs all fields Copy).

It’s not inheritance: no fields or methods are inherited — only trait impls are generated. The interview answer: #[derive(...)] auto-generates implementations of the listed traits for the type.

10. What is the difference between eprintln! and println! macros?

Answer: println! writes to stdout; eprintln! writes to stderr.

Both format and print with a newline, but to different standard streams:

  • println! → standard output (stdout) — normal program output.
  • eprintln! → standard error (stderr) — errors, warnings, diagnostics.

Why it matters: stdout is often buffered while stderr is unbuffered, and the two can be redirected independently (prog 2>err.log >out.log). Putting diagnostics on stderr keeps them out of the data stream and visible even when stdout is redirected. The interview answer: println! → stdout, eprintln! → stderr; same formatting, different streams.

11. How does Rust handle enum variants compared to languages like C or Java?

Answer: Rust enums are tagged unions (sum types) — each variant can carry its own payload of any types and shapes.

Unlike C enums (just named integers) or Java enums (classes with inheritance), Rust enums let each variant hold data:

enum Message {
    Quit,                              // unit variant, no data
    Move { x: i32, y: i32 },           // struct variant
    Write(String),                     // tuple variant
    ChangeColor(u8, u8, u8),           // tuple variant
}

Each variant is a different type shape; the enum is the sum of its variants. The compiler stores a discriminant (which variant) plus the active variant’s data, and match exhaustively destructures it. This makes enums the backbone of error handling (Result), optionals (Option), and state modeling. The interview answer: enums are tagged unions (sum types) whose variants can hold arbitrary typed payloads, unlike C/Java enums.

12. What will the following closure capture behavior produce?

fn main() {
    let mut num = 5;
    let mut add_num = move |x: i32| num += x;
    add_num(10);
    println!("{}", num);
}

Output: 5.

The move keyword forces the closure to own its captures. num is i32, which is Copy — so the closure gets its own copy of num. Inside the closure, num refers to that private copy:

  • add_num(10) adds 10 to the closure’s copy → its copy becomes 15.
  • The outer num is untouched → still 5.

Output: 5. The trap: move + Copy means “move a copy in,” not “mutate the original.” (Without move, the closure would capture &mut num and the outer value would become 15 — but then println! while the closure borrows would be a borrow error under NLL unless the closure’s last use ended.) The interview answer: 5move copies the i32 into the closure, so the outer num is unchanged.

13. What are the three closure traits in Rust, ordered from least to most restrictive on call frequency/environment?

Answer: Fn, FnMut, FnOnce — from least to most restrictive on the capture.

The three closure traits describe how a closure captures its environment and how often it can be called:

  • Fn — captures by immutable reference (&T). Can be called any number of times, even concurrently; doesn’t modify captured state. Least restrictive.
  • FnMut — captures by mutable reference (&mut T). Can be called repeatedly, mutating captured state, but not concurrently.
  • FnOnce — captures by value / consumes its environment (moves captures in). Can only be called once.
let x = 5;
let f = || println!("{}", x);        // Fn
let mut c = 0;
let mut f2 = || { c += 1; };         // FnMut
let s = String::from("hi");
let f3 = move || drop(s);            // FnOnce

The compiler picks the least restrictive trait a closure satisfies, and functions accepting closures can bound on Fn/FnMut/FnOnce accordingly. The interview answer: FnFnMutFnOnce, from least to most restrictive.

14. What is the purpose of the std::mem::forget function?

Answer: It consumes a value without running its Drop — an intentional leak or ownership transfer.

std::mem::forget(x) takes ownership of x and makes the compiler skip its destructor:

std::mem::forget(expensive_buffer);   // buffer's memory is never freed

Why ever do this?

  • FFI/ownership transfer: handing a value to C code that will free it later — you must forget it so Rust doesn’t also free it (double-free).
  • Static/global storage: moving a value into a static-like long-lived location where it must never be dropped.
  • Intentional leaks: deliberately keeping a resource alive for the program’s whole lifetime (rare).

It’s the inverse of the default: normally Rust drops everything automatically; forget opts out. Resources thus leaked are reclaimed only when the process exits. The interview answer: forget consumes a value while skipping its Drop, causing an intentional leak/transfer of ownership.

15. What does the zero-cost abstractions philosophy mean in Rust?

Answer: Language abstractions — generics, iterators, ownership — compile down to code as efficient as hand-written low-level code, with no runtime overhead.

“Zero-cost” (a principle Rust adopted from C++‘s Stroustrup) means: what you don’t use, you don’t pay for; what you do use, you couldn’t hand-code any better. Concretely:

  • Generics → monomorphized, no runtime dispatch or boxing.
  • Iterators/adaptors → optimized (unrolled, fused) into direct loops — a for x in v.iter().map(...).filter(...) is as fast as a manual loop.
  • Ownership/borrowing → decided entirely at compile time; no runtime GC or reference-counting.
  • Traits → static dispatch when possible; dyn only when you explicitly opt in.

The abstraction has conceptual cost (compile time, learning curve) but no runtime cost — compiled output matches hand-tuned code. The interview answer: high-level constructs add no runtime overhead vs hand-written low-level code, because the compiler optimizes them away.

My Private Notes

Notes are auto-saved locally to this device.