Collections, strings and iterators
String versus str, Vec and the map types, iterator adapters and consumers, closures and capturing, and ownership inside loops.
String, str and bytes
fn main() {
let owned: String = String::from("hello"); // heap, growable, owned
let borrowed: &str = &owned; // a view into the String
let literal: &'static str = "hello"; // baked into the binary
let mut s = String::new();
s.push_str("ab");
s.push('c');
// UTF-8: you cannot index by position, you slice by byte range
let text = "naïve café";
println!("{} bytes, {} chars", text.len(), text.chars().count());
for c in text.chars() { // scalar values
print!("{c} ");
}
for b in text.bytes() { // raw bytes
print!("{b} ");
}
let upper = text.to_uppercase(); // allocates: may change length
let parts: Vec<&str> = text.split(' ').collect();
let trimmed = " padded ".trim();
let joined = parts.join("-");
// the idiomatic parameter type for read-only text
fn shout(msg: &str) -> String {
format!("{}!", msg.to_uppercase())
}
println!("{}", shout(&joined).len() + trimmed.len());
}- Take
&stras a parameter and returnStringwhen you must allocate.&Stringas a parameter is a beginner's tell: it forces callers to own aStringfor no reason. - A
Stringis aVec<u8>that is always valid UTF-8.len()counts bytes,chars().count()counts scalar values, and neither equals visual characters — combining marks and emoji make grapheme clusters a separate concern. - Slicing with
&s[a..b]panics if the byte indices fall inside a character. Usechar_indices()orget()when the boundaries are not known. to_uppercaseandto_lowercasecan change the byte length, which is why they return an ownedStringwhiletrimreturns a slice.
Vec and the map types
use std::collections::{BTreeMap, HashMap};
fn main() {
let mut v: Vec<i32> = vec![3, 1, 2];
v.push(4);
v.sort_unstable();
v.retain(|n| *n != 1);
let sliced: &[i32] = &v[..2];
println!("{:?} {:?} {}", v, sliced, v.get(3).unwrap_or(&0));
// HashMap: fast, unordered, needs a Hash key
let mut counts: HashMap<String, u32> = HashMap::new();
for word in ["a", "b", "a"] {
// entry API: one lookup, no double hashing, no unwrap dance
*counts.entry(word.to_string()).or_insert(0) += 1;
}
// BTreeMap: ordered by key, used for deterministic output and range queries
let mut sorted: BTreeMap<&str, u32> = counts.iter().map(|(k, v)| (k.as_str(), *v)).collect();
for (k, n) in &sorted {
println!("{k}: {n}");
}
println!("{:?}", sorted.range("a"..="a").collect::<Vec<_>>());
// removing while iterating: collect the keys first
let to_drop: Vec<String> = counts.keys().filter(|k| k.len() > 3).cloned().collect();
for k in to_drop {
counts.remove(&k);
}
sorted.clear();
}| Type | Reach for it when |
|---|---|
Vec<T> | Ordered, indexable, contiguous; the default sequence |
VecDeque<T> | Push and pop at both ends, such as a queue |
HashMap<K, V> | Lookup by key, order irrelevant; needs Hash + Eq |
BTreeMap<K, V> | Sorted iteration, range queries, or a stable output order; needs Ord |
HashSet<T> | Membership tests and deduplication |
BinaryHeap<T> | Always take the largest (or smallest via Reverse) next |
Iterators and closures
#[derive(Debug)]
struct Order {
customer: String,
cents: u64,
}
fn main() {
let orders = vec![
Order { customer: "ada".into(), cents: 2500 },
Order { customer: "grace".into(), cents: 1800 },
Order { customer: "ada".into(), cents: 700 },
];
// adapters are lazy; a consumer drives them
let total: u64 = orders.iter().map(|o| o.cents).sum();
let big: Vec<&str> = orders
.iter()
.filter(|o| o.cents > 1000)
.map(|o| o.customer.as_str())
.collect();
let by_customer: HashMap<&str, u64> =
orders.iter().fold(HashMap::new(), |mut acc, o| {
*acc.entry(o.customer.as_str()).or_insert(0) += o.cents;
acc
});
// flat_map flattens one level: words from every customer name
let words: Vec<&str> = orders.iter().flat_map(|o| o.customer.split('-')).collect();
// borrowing inside a loop: iter() borrows, into_iter() consumes
let names: Vec<String> = orders.iter().map(|o| o.customer.clone()).collect();
for o in orders.iter() {
println!("{} {}", o.customer, o.cents); // orders still usable afterwards
}
for o in orders.into_iter() {
println!("{}", o.customer); // moves each element out
}
println!("{total} {big:?} {by_customer:?} {words:?} {names:?}");
}- Adapters such as
map,filterandtakedo nothing until a consumer such ascollect,sum,foldorfor_eachruns the chain — and the whole chain compiles to a loop with no intermediate allocation. iter()yields&Tand leaves the collection intact,iter_mut()yields&mut T, andinto_iter()consumes and yieldsT. Choosing wrongly is the most common source of "value moved here" errors in loops.collectneeds to know the target type: annotate the binding, or use the turbofishcollect::<Vec<_>>()in an expression.- A closure captures by reference when it can, and by mutable reference or move when it must. Adding
moveforces ownership transfer, which is required for threads and for returning closures from a function. - Chain order matters for cost:
filterbeforemapavoids work on discarded elements, andfindoranystop early wherecollectwould run the whole sequence.
⚠️
You cannot remove from a collection while iterating a borrow of it. The compiler rejects it, and the workaround is always the same shape: collect the keys or indices you want to remove, end the borrow, then remove them. Be glad the error appears at compile time — the equivalent loop in a language without borrow checking fails at run time on the element after the one you deleted.
FAQ
When do I use clone to satisfy the borrow checker?
When two real owners need independent data, or when a borrow would force lifetime parameters through code that does not care about them. Cloning a small
String at a boundary is fine; cloning a large collection inside a hot loop is a design problem.HashMap or BTreeMap?
HashMap when you only look values up by key and want the fastest path. BTreeMap when you iterate in key order, need a range query, or want output that is stable between runs and across platforms.Related
Traits, generics and lifetimes File I/O, serde and everyday crates
Last refreshed 2026-09-18.