18.xChapter 18 summary and quiz
The practical payoff chapter: the data structures you'll use in every program. Review, then a capstone that combines several.
Quick review
A collection is an owning handle to a variable amount of heap data (18.1), so it follows every ownership rule from chapter 8. Vec<T> is the growable list: build with vec![] or Vec::new, add with push, remove with pop (returns Option) (18.2). Access with [] (panics out of bounds, for known-good indices) or get (returns Option, for indices that might be invalid), the chapter-12 panic-versus-recover judgment applied to indexing. Iterate with for x in &vec (read), &mut vec (modify), or vec (consume) (18.3); modifying a vector while iterating is an E0502 borrow error, the chapter-9 rule catching a real bug. A vector tracks length (elements held) and capacity (room allocated); push reallocates a doubled buffer when full, and with_capacity pre-allocates when the size is known (18.4).
String is a UTF-8 byte buffer (18.5): len() counts bytes, s[0] doesn't compile (no good byte-or-char answer), chars() yields characters and bytes() yields bytes, and slicing panics if a byte index splits a character. HashMap<K, V> stores key-value pairs (18.6): insert, get (returns Option), and the entry(k).or_insert(d) idiom for update-or-insert; owned keys/values are moved in. Arrays [T; N] are fixed-length and stack-allocated, while slices &[T] are the universal view that accepts both arrays and vectors, so functions should take &[T] (18.7). And the wider family (18.8): VecDeque for both-ends access, HashSet for uniqueness, BTreeMap for sorted keys; default to Vec and HashMap.
Quiz time
Question #1
When should you use vec.get(i) versus vec[i]?
Show solution
Use vec[i] when the index is known to be valid and an out-of-bounds access would be a bug that should panic. Use vec.get(i) (returns Option<&T>) when the index might legitimately be out of range (user-supplied, probing), so you can handle None gracefully instead of crashing. Both are bounds-checked; the choice is whether out-of-range is a bug or an expected case.
Question #2
For "naïve" (where ï is two bytes), what do .len(), .chars().count(), and &s[0..2] produce?
Show solution
.len() is 6 (bytes: n, a, ï=2 bytes, v, e). .chars().count() is 5 (the five characters). &s[0..2] is "na", valid because byte 2 is a char boundary (after n and a, before ï). Slicing &s[0..3] would panic, because byte 3 falls inside the two-byte ï. (Bytes versus characters, lesson 18.5.)
Question #3
Why does the compiler reject pushing to a vector inside a for x in &vec loop?
Show solution
The loop holds an immutable borrow of the vector for its whole duration, and push needs a mutable borrow, which can't coexist (E0502, lesson 9.3). This prevents a real bug: push may reallocate the buffer (lesson 18.4), invalidating the loop's reference. The fix is retain, collecting changes for after the loop, or iterating indices.
Question #4
The capstone. Write a program that reads a sentence and reports word statistics. Given the string "the cat sat on the mat the cat", produce: the total word count, and a count of how many times each word appears. Use split_whitespace to get the words, a HashMap<&str, i32> with the entry API for the counts, and print the total plus each word's count.
Show solution
use std::collections::HashMap;
fn word_counts(text: &str) -> (usize, HashMap<&str, i32>) {
let mut counts: HashMap<&str, i32> = HashMap::new();
let mut total = 0;
for word in text.split_whitespace() {
total += 1;
*counts.entry(word).or_insert(0) += 1;
}
(total, counts)
}
fn main() {
let text = "the cat sat on the mat the cat";
let (total, counts) = word_counts(text);
println!("total words: {total}");
for (word, count) in &counts {
println!("{word}: {count}");
}
}total words: 8
the: 3
cat: 2
sat: 1
on: 1
mat: 1
(The per-word lines appear in an unspecified order, since HashMap iteration order isn't defined, lesson 18.6; only the counts are meaningful.) Design notes. split_whitespace yields &str views into text, so the map keys are &str (borrowing from text, which outlives the function call). *counts.entry(word).or_insert(0) += 1 is the update-or-insert idiom: insert 0 for a new word, then increment through the returned mutable reference. The function returns a tuple (lesson 4.11) of the total and the map, destructured in main. This small program uses a HashMap, the entry API, slices as keys, and iteration, the chapter's core in one place.
You've now got the core type system (chapters 8 to 17) and the everyday data structures (this chapter). You've been writing for loops over collections throughout, and you may have noticed methods like split_whitespace, chars, and retain that feel like they're doing something more powerful underneath. Chapter 19 reveals it: closures and iterators, where map, filter, and collect turn loops into expressive, lazy pipelines, and the language's functional heart finally shows itself.