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 &str as a parameter and return String when you must allocate. &String as a parameter is a beginner's tell: it forces callers to own a String for no reason.
  • A String is a Vec<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. Use char_indices() or get() when the boundaries are not known.
  • to_uppercase and to_lowercase can change the byte length, which is why they return an owned String while trim returns 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();
}
TypeReach 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, filter and take do nothing until a consumer such as collect, sum, fold or for_each runs the chain — and the whole chain compiles to a loop with no intermediate allocation.
  • iter() yields &T and leaves the collection intact, iter_mut() yields &mut T, and into_iter() consumes and yields T. Choosing wrongly is the most common source of "value moved here" errors in loops.
  • collect needs to know the target type: annotate the binding, or use the turbofish collect::<Vec<_>>() in an expression.
  • A closure captures by reference when it can, and by mutable reference or move when it must. Adding move forces ownership transfer, which is required for threads and for returning closures from a function.
  • Chain order matters for cost: filter before map avoids work on discarded elements, and find or any stop early where collect would 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.

Traits, generics and lifetimes File I/O, serde and everyday crates

Last refreshed 2026-09-18.