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

Top 50 - Part 3

Practice the final 20 questions from a comprehensive set of 50 important Rust programming interview questions.

1. What will be the output of this code snippet?

fn main() {
    let mut a = 1;
    let b = &mut a;
    *b += 2;
    println!("{}", a);
}

Output: 3.

*b += 2 adds 2 to a, making it 3. After that line, b is never used again — under NLL (Non-Lexical Lifetimes) the mutable borrow ends there, so println!("{}", a) can freely borrow a immutably. Output: 3.

(Without NLL, this would be a borrow error; with modern Rust it’s perfectly fine.) The interview answer: 3 — the mutable borrow ends after its last use, and a was incremented to 3.

2. How does Rust handle type conversion between primitive types (e.g., i32 to i64)?

Answer: Explicitly — via the as keyword or the From/Into traits; no implicit coercions.

Rust deliberately avoids implicit primitive conversions (they hide precision bugs, like i64 silently truncating to i32):

  • as casts: x as i64, y as f64, c as u8 — explicit, can truncate (must be intentional).
  • From/Into: i64::from(x) / x.into() — safe, lossless conversions (e.g., i32i64 is lossless so From exists; the reverse i64i32 is not From).
let i: i32 = 5;
let big: i64 = i as i64;      // explicit cast
let big2: i64 = i.into();     // via Into (lossless)

When a lossless conversion exists, prefer From/Into; use as when truncation/reinterpretation is intended. The interview answer: explicit — as for casts, From/Into for safe conversions; no implicit primitive coercions.

3. What is the difference between From and Into traits?

Answer: Implementing From<T> for U automatically provides Into<U> for T for free (blanket impl) — so implement From, use either.

From and Into are reflexive standard-library conversion traits:

impl From<MyId> for u32 {
    fn from(id: MyId) -> u32 { id.0 }
}
// Now both work:
let n: u32 = MyId(7).into();      // Into<u32> for MyId, auto-derived
let n2: u32 = u32::from(MyId(7)); // From directly

The standard library provides impl<T, U> Into<U> for T where U: From<T> — meaning From is the primary trait to implement, and Into comes along automatically. Idiomatic Rust: implement From; callers can use .into() ergonomically. The interview answer: From<T> for U implies Into<U> for T via a blanket impl, so implementing From gives you both.

4. What is the purpose of TryFrom and TryInto traits?

Answer: They handle fallible conversions that return a Result instead of panicking or truncating silently.

From/Into are infallible. TryFrom/TryInto cover conversions that can fail:

let big: i64 = 300;
let small: u8 = u8::try_from(big)?;   // Result<u8, TryFromIntError>
  • On success → Ok(value).
  • On failure (out of range, overflow) → Err(ErrorType) — no panic, no silent truncation.
impl TryFrom<i64> for MyType { /* returns Result */ }
let r: Result<MyType, _> = MyType::try_from(42);

They’re the safe alternative to truncating as casts, letting you handle the failure explicitly. The interview answer: TryFrom/TryInto perform conversions that can fail, returning Result rather than panicking.

5. What is the execution behavior of iter().map(…) on a slice or vector?

Answer: It’s lazy — transformations run only when a consumer (collect, for_each, next, sum, …) drives the iterator.

Iterators and their adaptors (map, filter, take, zip, …) build a pipeline without doing work:

let squares: Vec<i32> = v.iter()
    .filter(|x| **x > 2)      // nothing runs yet
    .map(|x| x * x)           // still nothing
    .collect();               // NOW the whole chain executes

Each adaptor wraps the previous iterator; the actual computation happens one element at a time as the terminal consumer pulls values. Benefits: no intermediate collections, no wasted work (.take(3) stops early), and the optimizer can fuse the chain into a tight loop. The interview answer: iterators are lazy — work happens only when a terminal consumer like .collect()/.for_each()/.next() drives them.

6. What does the .into_iter() method consume on a collections variable?

Answer: It consumes the collection, yielding owned values (T) — the original variable is moved and can’t be used afterward.

into_iter() takes self by value, producing an iterator of owned items:

let v = vec![1, 2, 3];
for x in v.into_iter() { /* x: i32 (owned) */ }
// v is MOVED — cannot use v here

The three iteration modes:

  • for x in &v / v.iter() → yields &T (borrows).
  • for x in &mut v / v.iter_mut() → yields &mut T (mutably borrows).
  • for x in v / v.into_iter() → consumes v, yields owned T; the collection is gone.

The ownership-erasing loop for x in v desugars to v.into_iter(). The interview answer: into_iter() consumes the collection (moves it) and yields owned values T.

7. What is the main utility of the std::mem::swap function?

Answer: It swaps the values at two memory locations in place, without violating ownership rules.

std::mem::swap(&mut a, &mut b) exchanges the values of two mutable references:

let mut x = 1;
let mut y = 2;
std::mem::swap(&mut x, &mut y);
// x == 2, y == 1

Why it matters:

  • It moves the values out and back without copying (no Clone needed, works on non-Copy types).
  • It’s the primitive behind many patterns — swap-to-remove from a Vec, RAII guards, temporarily replacing a value, implementing Default-based tricks.
  • It requires two distinct mutable borrows, so ownership rules are preserved.

There’s also std::mem::replace(&mut dest, src) — swap in a value and return the old one. The interview answer: mem::swap exchanges two values in place via mutable references, without copying or moving ownership out.

8. What is the size of an Option<Box<T>> in memory compared to a raw pointer?

Answer: Exactly the same size as a raw pointer — thanks to the Null Pointer Optimization (NPO).

Option<Box<T>> normally would need a discriminant + the pointer. But Rust’s NPO: since Box<T> can never be null, the compiler uses null (0x0) to represent None. So:

  • Some(box) → the pointer value.
  • None → pointer value 0.

Option<Box<T>> is therefore pointer-sized (8 bytes on 64-bit) — same as *const T — with zero extra storage. The same optimization applies to Option<&T>, Option<NonNull<T>>, Option<Vec<T>>, etc. (any type where a sentinel bit pattern is free). This is why Option is often “free” — no size or runtime cost over the inner pointer type. The interview answer: exactly pointer-sized, because NPO represents None as a null pointer.

9. What is the role of std::panic::catch_unwind?

Answer: It catches a panicking closure’s stack unwinding, returning Result, so a panic doesn’t propagate past a boundary (e.g., FFI).

let result = std::panic::catch_unwind(|| {
    // code that might panic
});
// Ok(value) or Err(Box<dyn Any + Send>)

Uses and constraints:

  • FFI safety: a panic unwinding across a C boundary is undefined behavior; wrap the boundary call in catch_unwind to contain it.
  • Isolation: catch panics in plugin/task code and continue.
  • Limitations: only catches unwinding panics (not panic = "abort" builds), and only across the current thread (panics in other threads aren’t caught).
  • The closure must be UnwindSafe (a &mut that’s already borrowed may not be).

The interview answer: catch_unwind runs a closure and captures an unwinding panic as Result, preventing unwinding past FFI/thread boundaries.

10. What will be the output of println!("{:?}", vec![1, 2, 3].iter().fold(0, |acc, x| acc + x))?

Output: 6.

.fold(0, |acc, x| acc + x) starts with an accumulator of 0 and adds each element: ((0 + 1) + 2) + 3 = 6. Output: 6.

fold (aka reduce) walks the iterator, threading an accumulated state through the closure — here summing the values. The interview answer: 6fold(0, +) sums 1+2+3.

11. What does the #[repr(C)] attribute do when applied to a Rust struct?

Answer: It forces C-compatible memory layout (field order and alignment) for FFI interoperability.

By default, Rust is free to reorder struct fields to minimize padding — the layout is unspecified. #[repr(C)] locks the layout to C’s rules: fields in declaration order, standard alignment/padding:

#[repr(C)]
struct Point { x: f64, y: f64 }   // exact C layout, guaranteed

Why it matters:

  • FFI: passing a struct to/from C code requires identical layout on both sides; without repr(C), the Rust side could arrange fields differently.
  • Stable layout: guarantees field offsets for unsafe code, manual serialization, or reading raw bytes.

You also get #[repr(u8)]/#[repr(i32)] etc. to control enum discriminant size, and #[repr(align(N))] for alignment. The interview answer: repr(C) forces C-compatible field ordering/padding, essential for FFI and stable layout guarantees.

12. What is the function of the include_str! macro?

Answer: It embeds a file’s contents into the binary at compile time as a &'static str.

include_str!("path/to/file.txt") reads the file at compile time and bakes its UTF-8 text directly into the executable:

const TEMPLATE: &str = include_str!("templates/email.html");
let schema: &'static str = include_str!("schema.sql");

Key points:

  • Compile-time — the file must exist when building; the string is baked into the binary (no runtime file I/O, no deployment dependency).
  • Returns &'static str (lifetime forever).
  • Sibling macros: include_bytes! (raw bytes as &'static [u8]), include! (include a Rust source file).

Use it for templates, SQL, embedded assets, and config that should ship inside the executable. The interview answer: include_str! reads a file at compile time and embeds its contents as a &'static str in the binary.

13. What does std::cell::Cell<T> provide for interior mutability?

Answer: Interior mutability for Copy types — values are changed by copying in/out, with no references handed out and no runtime borrow checks.

Cell<T> wraps a value and offers get()/set()/replace() that move or copy values:

let c = Cell::new(5);
c.set(10);
let v = c.get();   // v == 10 (Copy)

Key properties:

  • No references: Cell never yields &T or &mut T — it copies values in and out. So the aliasing rules can’t be violated, and no runtime borrow checks are needed (unlike RefCell).
  • Requires Copy: because get() returns by copy, Cell only works with Copy types (u32, bool, references, small structs).
  • Not thread-safe: single-threaded only (not Sync); use Mutex/Atomic across threads.

The interview answer: Cell gives interior mutability for Copy types by copying values in/out, avoiding borrow checks entirely — single-threaded only.

14. What will happen if you attempt to call .borrow_mut() on a RefCell<T> that already has an active .borrow() reference?

Answer: An immediate runtime panic (already borrowed: BorrowMutError).

RefCell<T> enforces the borrow rules at runtime. When an immutable borrow (.borrow()) is active and you call .borrow_mut():

  • The runtime detects two conflicting borrows (one shared read + one exclusive write).
  • The program panics immediately.
let cell = RefCell::new(5);
let r = cell.borrow();          // active immutable borrow
let m = cell.borrow_mut();      // PANIC: already borrowed

This is the trade-off versus compile-time borrow checking: RefCell accepts code the borrow checker would reject, but pays for it with runtime checks that panic on violation. The interview answer: it panics at runtime (BorrowMutError) — RefCell checks borrow conflicts dynamically.

15. What is the purpose of the non_exhaustive attribute on enums or structs?

Answer: It tells downstream crates the type may gain new variants/fields, forcing them to write wildcard arms (_ => ...) and blocking direct struct construction.

#[non_exhaustive] on a public type:

  • Enums: downstream crates can’t match exhaustively without a wildcard _ => ... arm — so the library can add variants later without breaking downstream matches.
  • Structs: downstream crates can’t construct the struct literally (missing fields would be an error), and can’t match fields exhaustively — so the library can add fields later without breaking construction.
#[non_exhaustive]
pub enum Error { Io, Parse }       // downstream must add `_ =>` arm

It’s a semver-stability tool for library authors: they can evolve the type without committing to a breaking change, at the cost of forcing downstream code to be non-exhaustive. The interview answer: #[non_exhaustive] forces downstream crates to add wildcard arms / avoid literal construction, letting the library add variants or fields in future versions without breaking them.

16. What is the behavior of Default::default() in Rust?

Answer: It constructs a default instance of a type implementing the Default trait.

Default::default() produces the type’s canonical “empty/sensible” value:

  • Numbers → 0, boolfalse, String"", Option<T>None, Vec<T> → empty.
#[derive(Default)]
struct Config { timeout: u64, retries: u32 }
let cfg = Config::default();   // timeout: 0, retries: 0

Uses: optional parameters (fill in what you don’t set), generic code needing a starting value (T::default()), builder patterns, and ..Default::default() to fill missing fields. The Default trait is often derived, but you can implement it manually for non-trivial defaults. The interview answer: Default::default() builds a standard initial value for the type — 0/""/None/empty collections etc.

17. What does the Sized marker trait indicate in Rust?

Answer: That the type’s size is known at compile time.

Sized is an auto-trait: T: Sized means the compiler knows T’s byte size at compile time, so values can live on the stack, in arrays, be passed by value, etc.

Unsized types (DSTs)str, [T] (slices), dyn Trait — have unknown size and must always sit behind a pointer (&str, &[T], Box<dyn Trait>), where the pointer carries the metadata (length or vtable).

let s: &str = "hi";          // str is unsized → behind &
let b: Box<[i32]> = vec![1,2,3].into_boxed_slice();  // [i32] unsized → behind Box

The interview answer: Sized means the type’s size is known at compile time; unsized types (str, [T], dyn Trait) must be used behind pointers.

18. What does ?Sized mean in a generic bound (e.g., <T: ?Sized>)?

Answer: It relaxes the default Sized bound, allowing T to be a dynamically sized type like [u8] or str.

By default, every generic parameter implicitly has T: Sized. T: ?Sized (“maybe sized”) opts out:

fn first<T: ?Sized>(s: &T) -> &T { s }   // T may be unsized
fn slice_len<T: ?Sized>(x: &T) -> usize { size_of_val(x) }

Why you’d want it: to write generic functions that also accept DSTs. Such functions must handle T behind a pointer (&T, Box<T>) since you can’t have a sized-by-value T. Cow, Box, and slice-related APIs use ?Sized so they work with both sized types and str/[T]. The interview answer: ?Sized removes the implicit Sized requirement, letting the parameter be a dynamically sized type (usually handled behind a reference/pointer).

19. How does Rust handle structural inheritance between types?

Answer: Rust has no classical OO struct inheritance — it uses composition and traits instead.

There’s no class B extends A in Rust. Code reuse and polymorphism are achieved via:

  • Composition: structs embedding other structs as fields.
struct Position { x: f64, y: f64 }
struct Entity { position: Position, name: String }   // has-a, not is-a
  • Traits: shared behavior (impl Trait for Type) — interface-style abstraction, not field inheritance.
  • Generics / trait objects: polymorphism without an inheritance tree.

You can get method-forwarding to an inner field via Deref (composition with delegation), but the language explicitly rejects the fragile “diamond inheritance” problems by not offering inheritance at all. The interview answer: no struct inheritance — composition (embedding) plus trait-based behavior provides reuse and polymorphism.

20. What does cfg(target_os = “windows”) do when used as an attribute?

Answer: It conditionally compiles the annotated item only when building for Windows.

#[cfg(...)] gates items on compile-time conditions:

#[cfg(target_os = "windows")]
fn platform_specific() { /* Windows-only code */ }

#[cfg(not(target_os = "windows"))]
fn platform_specific() { /* other platforms */ }
  • target_os is a compile-time configuration value set by the target triple ("windows", "linux", "macos", "android", …).
  • Items whose cfg condition is false are stripped from the build entirely (not compiled).

Other common cfg keys: target_arch ("x86_64", "aarch64"), debug_assertions, feature = "..." (Cargo features), unix/windows aliases. This is how Rust does cross-platform conditional code. The interview answer: #[cfg(target_os = "windows")] compiles the item only for Windows targets — conditional compilation.

My Private Notes

Notes are auto-saved locally to this device.