Menu

Earn Premium with Referrals

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

See how it works and start inviting friends.

Traits & Generics
RUST

Traits & Generics

Practice 22 Rust questions covering traits, generics, trait bounds, associated types, lifetimes, and trait objects.

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

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.

2. 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.

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.

3. 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).

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).

4. 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.

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.

5. 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.

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.

6. 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).

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).

7. 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.

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.

8. 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.

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.

9. 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.

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.

10. 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.

Answer:

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.

11. 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.

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.

12. 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.

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.

13. 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.

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.

14. 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.

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.

15. 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.

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.

16. 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.

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.

17. 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.

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.

18. 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.

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.

19. 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.

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.

20. 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.

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.

21. 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).

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).

22. 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.

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.

My Private Notes

Notes are auto-saved locally to this device.