Menu

Earn Premium with Referrals

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

See how it works and start inviting friends.

Strings & Collections
RUST

Strings & Collections

Practice 15 Rust questions covering String, str, Vec, HashMap, iterators, collection ownership, and common collection operations.

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 str only 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 str only 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 String value 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 String value 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 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.

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.

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: fiveSome(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: fiveSome(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() → 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.

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.

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: 6fold(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: 6fold(0, +) sums 1+2+3.

My Private Notes

Notes are auto-saved locally to this device.