Rust Basics #7 Collections and Iterators: Vec, String, HashMap, and Chains Instead of for
In Part 2 we said arrays have fixed length and promised that the real workhorses come later. This is that part: the growable list Vec, the string String, the key-value store HashMap, and the Rust way of working with them — iterators.
Vec: the everyday list #
let mut scores: Vec<u32> = Vec::new();
scores.push(90);
scores.push(85);
let scores = vec![90, 85, 72]; // literal macro
Vec<T> is a growable array with its data on the heap. Because it owns heap data, the ownership rules of Part 3 apply as-is, and the “no push during iteration” compile error from Part 4 had Vec as its stage. There are two ways to access an element, and the difference matters.
let third = scores[2]; // out of range → panic
let third = scores.get(2); // out of range → None (Option<&u32>)
Where the index is certainly valid by construction, use [2]; where the index comes from input or a computation, take it with get and handle the Option. The boundary drawn in Parts 5 and 6 — “impossible states panic, expected failures go into the type” — carries straight into the choice of access method.
String: why indexing is not allowed #
Rust’s String is a Vec of UTF-8 bytes. That is why code that feels natural in other languages does not compile.
let title = String::from("Rust Basics");
let first = title[0]; // compile error: String cannot be indexed
Part 2 said char is a 4-byte Unicode scalar, and Part 4 warned that slicing through the middle of a character panics. The answers converge here. In UTF-8, ASCII characters are one byte but accented letters are two, CJK characters three, emoji four — so title[0] has no guarantee of meaning “the first character.” One byte is not a character. Rather than returning “roughly the first byte,” Rust bans the operation and makes you say what you mean.
let word = String::from("naïve");
let first_char = word.chars().next(); // Some('n') — character view
let byte_len = word.len(); // 6 — length in bytes
let char_count = word.chars().count(); // 5 — number of characters
That len() returns bytes rather than characters is a classic trap in input-length validation, worth committing to memory. For building strings, push_str and the format! macro are the basic tools.
HashMap: key-value and the entry API #
use std::collections::HashMap;
let mut stock: HashMap<String, u32> = HashMap::new();
stock.insert(String::from("keyboard"), 12);
let count = stock.get("keyboard"); // Option<&u32>
That lookups return Option should be no surprise by now: a missing key is not null but None. The idiom worth learning is the entry API, which handles “insert if absent, update if present” in a single lookup.
let mut word_count: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*word_count.entry(word).or_insert(0) += 1;
}The three-step dance — “check if the key exists → insert if not → look up again and modify” — becomes one line. It is the standard form of aggregation code like word counting.
Iterators: the chain that replaces for #
With the three collections in hand, here is the Rust way of processing them. “Sum of scores of 80 or above,” written twice:
// the for way
let mut total = 0;
for s in &scores {
if *s >= 80 {
total += s;
}
}
// the iterator chain way
let total: u32 = scores.iter().filter(|s| **s >= 80).sum();The |s| ... passed to filter is a closure — an anonymous function. The chain style is the idiom not by taste but by structure: the mut variable disappears, and “filter, then sum” is expressed by names rather than by the order of operations. Use map for transformation and collect to gather results into a new collection.
let names: Vec<String> = users.iter()
.filter(|u| u.active)
.map(|u| u.name.clone())
.collect();Two more facts complete the picture. First, iterators are lazily evaluated: map and filter only stack up a plan, which runs in one pass at a consuming call like collect or sum, so no intermediate collections are built. Compiled performance is on par with a handwritten loop — this is not a trade of speed for expressiveness. Second, iteration also comes in ownership flavors: iter() borrows and iterates by reference, while into_iter() takes ownership as it goes. If you will keep using the collection afterwards, iter() is the default.
Summary #
Vecis the everyday list. Structurally certain indexes use[i]; indexes from the outside world go throughgetand itsOption.Stringis a Vec of UTF-8 bytes, so indexing is banned. Characters go throughchars(), andlen()counting bytes is the practical trap.HashMaplookups returnOption, and aggregation uses theentry(...).or_insert(...)idiom.- The default form of collection processing is the
iter().filter().map().collect()chain — lazily evaluated, compiling to the same speed as a handwritten loop. - Next part: traits and generics. The IOUs accumulated so far —
derive(Debug), theCopytrait,Box<dyn Error>— all get settled at once.