December Code

Rust iterators and collections interview questions, with answers

Idiomatic Rust processes data with iterators: chains of adapters such as map and filter, ended by a consumer such as collect or sum. They compile to code as fast as a hand-written loop, so interviewers expect you to reach for them and to understand how they work: which method borrows and which consumes, why nothing happens until a consumer runs, and how collect decides what to build.

The answers below cover the standard collections, the three ways to iterate, laziness, collect, sorting, closures and adapter chains, with code compiled and run on rustc 1.98. Then take the free Rust diagnostic — ten questions across every Rust topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What are the main collections in Rust's standard library?

    In short: Vec for growable sequences, VecDeque for queues, HashMap and HashSet for hashing, BTreeMap and BTreeSet for sorted data, and String for text.

    Vec<T> is the default sequence: contiguous, growable and indexable. VecDeque<T> adds cheap pushes and pops at both ends. HashMap<K, V> and HashSet<T> give average constant-time lookups; their keys must implement Eq and Hash. BTreeMap and BTreeSet keep their keys sorted, with logarithmic operations, which also makes iteration ordered. BinaryHeap is a priority queue. All of them own their elements and free them when dropped. HashMap::get returns an Option, so a missing key is handled like any other absent value, and the entry API updates or inserts in a single lookup.

    use std::collections::HashMap;
    
    fn main() {
        let mut m = HashMap::new();
        m.insert("ann", 30);
        m.insert("bo", 25);
        let a = m.get("ann");
        let n = m.len();
        println!("{a:?} {n}");
        // Some(30) 2
    }
  2. 2.What is the difference between iter, iter_mut and into_iter in Rust?

    In short: iter borrows each element as &T, iter_mut borrows each as &mut T, and into_iter consumes the collection and yields owned T values.

    The three methods differ in what the loop gets. iter() produces shared references, so the collection is only read and remains usable. iter_mut() produces mutable references, allowing elements to be changed in place, as the loop that multiplies by 10 does below. into_iter() takes ownership of the collection and yields the elements by value, so the collection cannot be used afterwards, which is useful when moving elements somewhere else. A for loop calls into_iter on whatever it is given, so for x in &v iterates by reference and for x in v consumes v.

    let mut v = vec![4, 5, 6];
    for x in v.iter_mut() {
        *x *= 10;
    }
    let s: i32 = v.iter().sum();
    let w: Vec<i32> = v.into_iter()
        .rev().collect();
    println!("{s} {:?}", w);
    // 150 [60, 50, 40]
  3. 3.Why are Rust iterators lazy?

    In short: Adapters such as map and filter only build a description of the work; nothing runs until a consumer such as collect, sum or a for loop asks for items.

    Calling map or filter wraps the iterator in a new struct that remembers the closure; no element is touched yet. A consumer, such as collect, count, sum or a for loop, then pulls items through the whole chain one at a time. That design means a chain does a single pass without intermediate collections, can stop early, as take(3) does below, and can even work on an infinite source like the range 1.. . The compiler inlines the adapters, so the chain runs as fast as a hand-written loop. An adapter chain that is never consumed does nothing, and the compiler warns about it.

    let n = (1..)
        .filter(|x| x % 3 == 0)
        .take(3)
        .count();
    println!("{n}");
    // 3
  4. 4.How does collect know what collection to build in Rust?

    In short: collect can build any type that implements FromIterator, and it picks the target from the type annotation or the turbofish, such as Vec<_> or BTreeSet<_>.

    collect is generic over its return type: anything implementing FromIterator for the item type can be built, including Vec, String, HashMap, the set types, and even Result<Vec<T>, E>, which stops at the first error. The target comes from context, usually an annotation on the variable, and the element type can often be left as _ for the compiler to infer. Collecting the same three items into a BTreeSet removes the duplicate and sorts them, while a Vec keeps every item in order, as below. collect::<Vec<_>>() is the turbofish form when there is no variable to annotate.

    use std::collections::BTreeSet;
    
    fn main() {
        let w = ["b", "a", "b"];
        let s: BTreeSet<_> =
            w.iter().collect();
        let v: Vec<_> =
            w.iter().collect();
        println!("{s:?}");
        // {"a", "b"}
        println!("{v:?}");
        // ["b", "a", "b"]
    }
  5. 5.How do you sort a Vec in Rust?

    In short: Use sort or sort_unstable for types with a total order, sort_by or sort_by_key for a custom order, and f64::total_cmp for floats, which lack Ord.

    v.sort() sorts in place and is stable, keeping equal elements in their original order; sort_unstable is usually faster and uses no extra memory. Both require the element type to implement Ord. sort_by takes a comparison closure and sort_by_key takes a closure that extracts a key, such as sorting users by age. Floating-point numbers implement only PartialOrd, because NaN is not ordered, so v.sort() does not compile for a Vec<f64>; passing f64::total_cmp to sort_by, as below, gives a total order in which NaN sorts after every other value.

    let mut v = vec![3.2, 1.5];
    v.sort_by(f64::total_cmp);
    println!("{:?}", v);
    // [1.5, 3.2]
  6. 6.What are closures in Rust, and what are Fn, FnMut and FnOnce?

    In short: Closures are anonymous functions that capture their environment; the traits say how: Fn only reads it, FnMut changes it, and FnOnce may consume it and so runs once.

    A closure such as || hits += 1 captures hits and changes it, so it implements FnMut, and the closure itself must be declared mut to call it. A closure that only reads its captures implements Fn and can be called any number of times, even concurrently; one that moves a captured value out, for example by returning it, implements only FnOnce. Every Fn closure is also FnMut and FnOnce. Functions that take closures state the least they need, such as map taking FnMut. The capture ends when the closure is last used, which is why hits can be printed afterwards below.

    let mut hits = 0;
    let mut tap = || hits += 1;
    tap();
    tap();
    println!("{hits}");
    // 2
  7. 7.How do chained iterator adapters like zip, map and filter work in Rust?

    In short: Each adapter wraps the previous iterator, so a chain processes one item at a time through every step, ending in a consumer such as collect.

    zip pairs two iterators element by element, stopping at the shorter one. map transforms each item, here multiplying each pair, and filter keeps the items for which its closure returns true, receiving each item by reference. Because each adapter pulls from the one before it, the first product, 10, passes through filter before the second pair is even read, and no intermediate vector is created. The result is collected once at the end. Other common adapters are enumerate, which adds an index, flat_map, take_while, skip and chain, and consumers include fold, any, all, position and max_by_key.

    let a = [1, 2, 3];
    let b = [10, 20, 30];
    let v: Vec<i32> = a.iter()
        .zip(b.iter())
        .map(|(x, y)| x * y)
        .filter(|p| p % 20 != 0)
        .collect();
    println!("{:?}", v);
    // [10, 90]

How the diagnostic asks it

One question from the Rust bank, exactly as a sitting would show it. The bank has 3 on collections & iterators and 30 across Rust.

Collections & Iterators · mediumRUST-022

What does this Rust code print?

let v = vec![1, 2, 3];
let it = v.iter().map(|x| {
    print!("m{} ", x);
    x * 10
});
print!("start ");
let out: Vec<i32> = it.collect();
println!("{:?}", out);
  1. 1m1 m2 m3 start [10, 20, 30]
  2. 2start [10, 20, 30]
  3. 3It does not compile: a map closure cannot print
  4. 4start m1 m2 m3 [10, 20, 30]correct

Calling map does not run anything: it wraps the iterator in a new one that will apply the closure when asked for an item. So after the let, nothing has been printed, and start comes first. collect then pulls the items one by one, running the closure for each and printing m1 m2 m3, and the vector [10, 20, 30] is printed last. The option with the closures first treats map as eager, like a list operation in some languages. start [10, 20, 30] forgets that the closure still runs, inside collect. Closures can have side effects such as printing. An unused map is even flagged by the compiler: iterators are lazy and do nothing unless consumed.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 Rust questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated