Common Collections
Vec, String, HashMap — the heap-allocated workhorses and their ownership rules.
Part of Rust Interview Mastery · Based on The Rust Programming Language
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.)