Rust Interview Mastery · Chapter 08 08

Common Collections

Vec, String, HashMap — the heap-allocated workhorses and their ownership rules.

11 questions 3 topics Intermediate

Vectors

Vec basics — creation, growth, and cleanup.
let mut v: Vec<i32> = Vec::new();
v.push(5); v.push(6);
let v2 = vec![1, 2, 3];          // macro with initial values

Vec<T> is a growable heap array — contiguous memory, amortized O(1) push (doubles capacity when full). When v goes out of scope, the Vec and all its elements drop — one owner frees everything. vec![] is the macro for literals; Vec::new needs the type annotation until the first push lets inference lock in.

v[i] vs v.get(i) — what's the difference and when does each win?
let third = &v[2];        // panics if out of bounds
let third = v.get(2);     // returns Option<&T>

Index syntax is for "this must exist — bug if it doesn't"; get is for "maybe exists — handle it." In a real system: index for loop-internal provable positions, get for user-facing lookups. Matching None explicitly beats a panic — that's chapter 9's philosophy applied one level early.

Why does this fail — holding a reference while pushing to a Vec?
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);                 // ERROR: cannot borrow as mutable
println!("{first}");

push may reallocate — when capacity is exceeded the Vec moves its buffer to a bigger heap allocation, and first would dangle into freed memory. The borrow checker sees the live & borrow, blocks the &mut needed by push, and deletes an entire class of iterator/pointer invalidation bugs.

How do you store different types in one Vec? Explain the enum trick and its trade-off.
enum Cell { Int(i32), Float(f64), Text(String) }
let row = vec![Cell::Int(3), Cell::Text("a".into()), Cell::Float(9.1)];

Vec<T> needs one T — so you define a T that is one of your types. Everything downstream matches on the variant, which is the win: the compiler forces handling of every case.

Trade-off vs trait objects (Vec<Box<dyn Trait>>, ch 18): enum is closed (variants fixed at compile time, faster dispatch, exhaustive checking); trait objects are open (any type implementing the trait, dynamic dispatch). Closed set → enum; plugin-like extensibility → trait object.

Strings

String vs &str — draw the line for an interview.

String = owned, growable, heap-allocated UTF-8 (Vec<u8> with UTF-8 guarantees). &str = borrowed view into UTF-8 text owned elsewhere — a slice.

Choose String when the code owns/stores/mutates text; &str for parameters that just read text (accepts String, literals, sub-slices via deref coercion). Literals are &'static str. In API design: take &str, return String when producing new text.

Why can't you index into a Rust String with s[0]?

Because UTF-8 is variable-width — a char is 1–4 bytes, and even a "character" (grapheme cluster) can be several chars. s[0] could split a multi-byte character and produce meaningless data; worse, O(1) indexing can't mean what you'd expect when positions don't map to characters.

Rust forces the question "bytes, chars, or words?": s.chars() for Unicode scalar values, s.bytes() for raw bytes, s[0..4] for byte ranges (panics on non-char-boundary). The friction is intentional — string indexing is where subtle i18n bugs live.

Show the ways to build and extend Strings, and what each does to ownership.
let mut s = String::from("a");
s.push_str("bc");        // append &str — borrows, nothing moves
s.push('!');              // single char
let s3 = s1 + &s2;        // consumes s1! (fn add(self, s: &str))
let s = format!("{a}-{b}"); // no ownership moves, most flexible

The + operator is the trap: s1 + &s2 moves s1 — it calls add(self, &str), and s1 is unusable after. Great when concatenating chains where you'd drop the left side anyway; surprising otherwise. format! is the general-purpose answer — readable, no moves.

chars() vs bytes() on a string — when does it matter?
"नमस्ते".bytes().count()   // 18 (3 bytes per Devanagari char)
"नमस्ते".chars().count()   // 6

bytes() is the raw UTF-8 encoding — right for hashing, wire protocols, size limits. chars() yields Unicode scalar values — right for "character" logic like truncation. Neither gives user-perceived graphemes (é can be 2 chars); for that you need a crate like unicode-segmentation. In an interview, knowing which question to ask is the point.

HashMaps

HashMap basics — insert, get, and what happens to ownership.
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert(String::from("blue"), 10);   // String key moves INTO the map
m.get("blue");                        // Option<&V>
for (k, v) in &m { /* iterate by ref */ }

insert takes ownership of both key and value if they're owned types (String), or copies Copy types. get returns Option<&V> — borrow, not ownership; you can't get an owned value out without remove. K needs Eq + Hash. Iterating order is unspecified — never rely on it.

Explain the entry API — the word-counting idiom every Rust interview loves.
for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

entry(key) returns an Entry enum — Occupied or Vacant. or_insert(0) gives &mut V: existing value's ref, or insert-then-ref. One lookup instead of contains + insert — and the borrow checker sees a single borrow, so it's both clean and efficient.

This is THE canonical HashMap pattern; it shows ownership (&mut V), the Entry type, and a real algorithm in four lines.

How does Rust's HashMap defend against hash-flooding, and why does that matter?

The default hasher is SipHash 1-3 — a cryptographically-ish keyed hash designed to resist hash-flooding DoS: an attacker who can predict bucket collisions crafts keys that all land in one bucket, degrading O(1) lookups to O(n) chains and hanging your server. SipHash's per-map random key foils that.

It's slower than simple hashes — so you can swap in FnvHashMap/AHash for performance when keys aren't attacker-controlled. Interview answer: default is secure-first, and the trade-off is documented and configurable. (Additional context beyond the book's brief mention.)