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):
ascasts: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.,i32→i64is lossless soFromexists; the reversei64→i32is notFrom).
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()→ consumesv, yields ownedT; 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
Cloneneeded, works on non-Copytypes). - It’s the primitive behind many patterns — swap-to-remove from a
Vec, RAII guards, temporarily replacing a value, implementingDefault-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 value0.
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_unwindto 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&mutthat’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: 6 — fold(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:
Cellnever yields&Tor&mut T— it copies values in and out. So the aliasing rules can’t be violated, and no runtime borrow checks are needed (unlikeRefCell). - Requires
Copy: becauseget()returns by copy,Cellonly works withCopytypes (u32,bool, references, small structs). - Not thread-safe: single-threaded only (not
Sync); useMutex/Atomicacross 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,bool→false,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_osis a compile-time configuration value set by the target triple ("windows","linux","macos","android", …).- Items whose
cfgcondition 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.
Premium Content
Unlock Top 50 - Part 3 and all premium lessons with a subscription.
From ₹199.99/year — See plans