1. 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.
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.
2. What does the str slice type (&str) represent in Rust?
Answer: An immutable view/slice over UTF-8 encoded string data — a borrowed reference to string bytes, not an owner.
&str (a string slice) is a (&[u8], len) pair: a pointer to UTF-8 bytes plus a length. It borrows data that lives elsewhere — in a String’s heap buffer, in static binary memory (string literals), or in a &[u8]:
let owned: String = String::from("hello");
let view: &str = &owned; // borrows owned's buffer
let lit: &str = "hello"; // points into the binary
Properties:
- Immutable — you can’t modify string data through
&str(use&mut stronly for ASCII-heavy cases, rarely). - No ownership — the backing storage must outlive the slice (lifetime-checked).
- UTF-8 — bytes are guaranteed valid UTF-8.
Contrast: String is the owned, growable heap buffer; &str is the cheap borrowed view. The interview answer: &str is an immutable, borrowed view of UTF-8 string bytes living elsewhere.
Answer:
An immutable view/slice over UTF-8 encoded string data — a borrowed reference to string bytes, not an owner.
&str (a string slice) is a (&[u8], len) pair: a pointer to UTF-8 bytes plus a length. It borrows data that lives elsewhere — in a String’s heap buffer, in static binary memory (string literals), or in a &[u8]:
let owned: String = String::from("hello");
let view: &str = &owned; // borrows owned's buffer
let lit: &str = "hello"; // points into the binary
Properties:
- Immutable — you can’t modify string data through
&str(use&mut stronly for ASCII-heavy cases, rarely). - No ownership — the backing storage must outlive the slice (lifetime-checked).
- UTF-8 — bytes are guaranteed valid UTF-8.
Contrast: String is the owned, growable heap buffer; &str is the cheap borrowed view. The interview answer: &str is an immutable, borrowed view of UTF-8 string bytes living elsewhere.
3. What is the size of a char in Rust?
Answer: 4 bytes (32 bits) — it holds a Unicode Scalar Value.
A Rust char is always a fixed 4 bytes, representing a single Unicode Scalar Value (any code point except surrogates):
let a: char = 'a'; // 4 bytes
let z: char = 'ℤ'; // 4 bytes
let crab: char = '🦀'; // 4 bytes
Key distinction: char (4 bytes) vs UTF-8 encoding (1–4 bytes per character). The same character encodes to 1–4 bytes in a String/&str, but as a char value it always occupies 4 bytes regardless of the character. This makes char arrays/vectors uniform and indexable by scalar value.
This differs from C (char = 1 byte), Java (char = 2 bytes, UTF-16 code unit). The interview answer: 4 bytes — a Unicode scalar value, independent of its UTF-8 encoded length.
Answer:
4 bytes (32 bits) — it holds a Unicode Scalar Value.
A Rust char is always a fixed 4 bytes, representing a single Unicode Scalar Value (any code point except surrogates):
let a: char = 'a'; // 4 bytes
let z: char = 'ℤ'; // 4 bytes
let crab: char = '🦀'; // 4 bytes
Key distinction: char (4 bytes) vs UTF-8 encoding (1–4 bytes per character). The same character encodes to 1–4 bytes in a String/&str, but as a char value it always occupies 4 bytes regardless of the character. This makes char arrays/vectors uniform and indexable by scalar value.
This differs from C (char = 1 byte), Java (char = 2 bytes, UTF-16 code unit). The interview answer: 4 bytes — a Unicode scalar value, independent of its UTF-8 encoded length.
4. What does String::from(“hello”) allocate?
Answer: A growable, UTF-8 buffer on the heap, plus a stack-side handle holding pointer, length, and capacity.
String is the owned, resizable string type. Its layout:
- Heap: the actual UTF-8 bytes in a growable buffer (can reallocate to grow).
- Stack: the
Stringvalue itself — three words: a pointer to the heap buffer, the current length (bytes), and the capacity (allocated bytes).
let s = String::from("hello"); // heap: h,e,l,l,o stack: (ptr, 5, 5)
This three-word structure is why a String is cheap to move and resize: moving copies the handle, growing reallocates the heap buffer and updates the metadata. When the String is dropped, the heap buffer is freed (RAII). The interview answer: a heap-allocated UTF-8 buffer with a stack handle (pointer, length, capacity).
Answer:
A growable, UTF-8 buffer on the heap, plus a stack-side handle holding pointer, length, and capacity.
String is the owned, resizable string type. Its layout:
- Heap: the actual UTF-8 bytes in a growable buffer (can reallocate to grow).
- Stack: the
Stringvalue itself — three words: a pointer to the heap buffer, the current length (bytes), and the capacity (allocated bytes).
let s = String::from("hello"); // heap: h,e,l,l,o stack: (ptr, 5, 5)
This three-word structure is why a String is cheap to move and resize: moving copies the handle, growing reallocates the heap buffer and updates the metadata. When the String is dropped, the heap buffer is freed (RAII). The interview answer: a heap-allocated UTF-8 buffer with a stack handle (pointer, length, capacity).
5. What is the output of the following slice operation?
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
println!("{}", hello);
}
Output: hello.
&s[0..5] slices byte indices 0 through 4 (inclusive of start, exclusive of end). The first five bytes of "hello world" are h,e,l,l,o, so the slice is "hello". Output: hello.
(These indices are byte offsets, and &s[a..b] will panic if they don’t land on UTF-8 character boundaries — here the string is ASCII so any split is fine.) The interview answer: hello — &s[0..5] takes bytes 0–4.
Answer:
hello.
&s[0..5] slices byte indices 0 through 4 (inclusive of start, exclusive of end). The first five bytes of "hello world" are h,e,l,l,o, so the slice is "hello". Output: hello.
(These indices are byte offsets, and &s[a..b] will panic if they don’t land on UTF-8 character boundaries — here the string is ASCII so any split is fine.) The interview answer: hello — &s[0..5] takes bytes 0–4.
6. What happens if a string slice index falls in the middle of a multi-byte UTF-8 character?
Answer: A runtime panic — Rust won’t create a slice on a non-character boundary.
&s[a..b] is checked at runtime: the byte index must lie on a UTF-8 character boundary. If a or b splits a multi-byte character, slicing panics rather than producing invalid UTF-8.
let s = "héllo"; // 'é' is 2 bytes
let bad = &s[1..3]; // panics: byte 1 is inside 'é'
Why panic instead of returning a result: the Index operator has no way to signal failure gracefully, and silently producing a broken slice would violate the UTF-8 invariant. For safe boundary handling, use char_indices() to find real boundaries or the get()/get_mut() methods which return Option:
let ok = s.get(2..3); // Option<&str> — None if not a boundary
The interview answer: it panics at runtime because indices must fall on valid UTF-8 character boundaries.
Answer:
A runtime panic — Rust won’t create a slice on a non-character boundary.
&s[a..b] is checked at runtime: the byte index must lie on a UTF-8 character boundary. If a or b splits a multi-byte character, slicing panics rather than producing invalid UTF-8.
let s = "héllo"; // 'é' is 2 bytes
let bad = &s[1..3]; // panics: byte 1 is inside 'é'
Why panic instead of returning a result: the Index operator has no way to signal failure gracefully, and silently producing a broken slice would violate the UTF-8 invariant. For safe boundary handling, use char_indices() to find real boundaries or the get()/get_mut() methods which return Option:
let ok = s.get(2..3); // Option<&str> — None if not a boundary
The interview answer: it panics at runtime because indices must fall on valid UTF-8 character boundaries.
7. 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.
Answer:
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.
8. 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 theIndextrait; ifi >= len, the program panics at runtime. Fast, no allocation, but can crash.vec.get(i)— returnsOption<&T>:Some(&value)if in bounds,Noneotherwise. 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.
Answer:
Direct indexing panics on out-of-bounds; .get() safely returns None.
vec[i]— uses theIndextrait; ifi >= len, the program panics at runtime. Fast, no allocation, but can crash.vec.get(i)— returnsOption<&T>:Some(&value)if in bounds,Noneotherwise. 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.
9. 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.
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.
10. What will be the output of this pattern matching expression?
fn main() {
let x = Some(5);
if let Some(5) = x {
println!("five");
} else {
println!("other");
}
}
Output: five.
if let matches the scrutinee against the pattern. x is Some(5), and the pattern Some(5) matches exactly (value 5). So the if branch runs, printing five.
(if let is the ergonomic form of match for single-pattern tests: match x { Some(5) => ..., _ => ... }.) The interview answer: five — Some(5) matches the pattern Some(5).
Answer:
five.
if let matches the scrutinee against the pattern. x is Some(5), and the pattern Some(5) matches exactly (value 5). So the if branch runs, printing five.
(if let is the ergonomic form of match for single-pattern tests: match x { Some(5) => ..., _ => ... }.) The interview answer: five — Some(5) matches the pattern Some(5).
11. Which macro is used to create custom formatted strings without printing them to standard output?
Answer: format!.
format! uses the same format-specifier machinery as println! but returns an owned String instead of printing:
let name = "world";
let s = format!("Hello, {name}!"); // s == "Hello, world!"
The family: print!/println! → stdout; eprint!/eprintln! → stderr; write!/writeln! → any Write destination (file, buffer); format! → a String. (sprintf! doesn’t exist in Rust — it’s C.) The interview answer: format! — it formats into a String without printing.
Answer:
format!.
format! uses the same format-specifier machinery as println! but returns an owned String instead of printing:
let name = "world";
let s = format!("Hello, {name}!"); // s == "Hello, world!"
The family: print!/println! → stdout; eprint!/eprintln! → stderr; write!/writeln! → any Write destination (file, buffer); format! → a String. (sprintf! doesn’t exist in Rust — it’s C.) The interview answer: format! — it formats into a String without printing.
12. 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.
Answer:
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.
13. 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.
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.
14. 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.
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.
15. 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.
Answer:
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.
Premium Content
Unlock Strings & Collections and all premium lessons with a subscription.
From ₹199.99/year — See plans