Rust Fundamentals

Collections and Iteration


Learning Objectives

  • You can use vectors to store variable-length collections of data.
  • You understand how to work with HashMaps for key-value storage.
  • You know the nuances of String manipulation and why indexing is tricky.
  • You can use iterators to process collections efficiently.
  • You understand common iterator methods like map, filter, and collect.
  • You know how to use peekable iterators for lookahead scenarios.

Warming up

Here is a set of tiny programming tasks to get you familiar with syntax that relates to collections and iteration. You can solve these exercises directly in the embedded code editor.

Loading Exercise...

Vectors

We’ve seen arrays in previous chapters, but they have a fixed size. We’ve also implemented our own linked list while learning about Box for recursive types, but that’s not very efficient for many use cases.

Rust also comes with Vectors (Vec<T>), which are growable arrays that can expand and shrink as needed. There is also a macro vec! for easy vector creation.

Creating Vectors

There are several ways to create a vector:

Note: Vectors store their data on the heap, not the stack. This allows them to grow dynamically. The vector itself (which contains a pointer, length, and capacity) lives on the stack, but the actual data lives on the heap.

Loading Exercise...

Adding and Removing Elements

Accessing Elements

There are two ways to access vector elements: indexing and the get method.

Note: Use get() when the index might be invalid. Use direct indexing ([]) only when you’re certain the index is valid. Panicking in production code is usually a bad idea!

Loading Exercise...

Iterating Over Vectors


Loading Exercise...

Vectors and Ownership

Ownership rules apply to vectors too:


Loading Exercise...

Storing Different Types

Vectors must store elements of the same type. But sometimes you need flexibility:

enum MenuItem {
    Drink { name: String, price: f64 },
    Food { name: String, price: f64, calories: u32 },
    Dessert { name: String, price: f64, sugar_free: bool },
}

fn main() {
    let menu = vec![
      MenuItem::Drink {
        name: String::from("Espresso"),
        price: 2.50,
      },
      MenuItem::Food {
        name: String::from("Croissant"),
        price: 3.00,
        calories: 350,
      },
      MenuItem::Dessert {
        name: String::from("Brownie"),
        price: 4.50,
        sugar_free: false,
      },
    ];

    for item in &menu {
      match item {
        MenuItem::Drink { name, price } => {
          println!("{}: {:.2} euros (drink)", name, price);
        }
        MenuItem::Food { name, price, calories } => {
          println!("{}: {:.2} euros ({} cal)", name, price, calories);
        }
        MenuItem::Dessert { name, price, sugar_free } => {
          let sf = if *sugar_free { " (sugar-free)" } else { "" };
          println!("{}: {:.2} euros{}", name, price, sf);
        }
      }
    }
}

HashMaps

While vectors use numeric indices, HashMaps store key-value pairs where you can use any type as the key (as long as it implements the Eq and Hash traits).

Creating HashMaps

Unlike vectors, HashMaps aren’t automatically imported. You need to bring them into scope:

Accessing Values

Updating Values


Loading Exercise...

The Entry API

The entry API is powerful for updating values based on whether they exist:


Loading Exercise...

Iterating Over HashMaps

Note: HashMaps don’t maintain any particular order. If you need ordering, consider using BTreeMap instead (also in std::collections).

Loading Exercise...

Strings Revisited

We’ve used strings throughout this course, but let’s dive deeper. Rust has a complex relationship with strings because it prioritizes correctness and performance.

String vs &str

As we learned earlier:

  • String is an owned, growable string on the heap
  • &str is a borrowed string slice

Building Strings

Why You Can’t Index Strings

In many languages, you can access string characters by index. Not in Rust:

let drink = String::from("Latte");
let first = drink[0];  // Error!

Why? Because Rust strings are UTF-8 encoded. A character might be 1, 2, 3, or 4 bytes. Indexing by byte position could split a character in half!


Loading Exercise...

Iterating Over Strings

Since indexing doesn’t work, use iteration:

Common String Methods


Loading Exercise...

Iterators

Iterators are a powerful feature in Rust. We’ve been using them with for loops, but there’s much more to them.

What is an Iterator?

An iterator is something that lets you process a sequence of elements. In Rust, iterators are lazy - they don’t do anything until you consume them.


Loading Exercise...

Three Ways to Get Iterators

Iterator Methods

Iterators have many powerful methods. These are called iterator adapters because they produce new iterators:

Note: Notice that map and filter don’t do anything by themselves - they’re lazy. You need to call collect() or another consuming method to actually process the iterator.

Common Iterator Consumers

These methods consume the iterator and produce a final result:


Loading Exercise...

The fold Method

fold is a powerful method that lets you accumulate a value:


Loading Exercise...

Chaining Multiple Operations

The real power comes from chaining multiple iterator methods:


Loading Exercise...

Example: Coffee Shop Analytics

The following shows an analytics system that demonstrates the collection and iteration concepts:

The example demonstrates:

1. Vector Usage

struct SalesAnalytics {
  sales: Vec<Sale>,  // Stores all sales
}

We use a vector to store all sales records.

2. HashMap for Aggregation

fn sales_by_drink(&self) -> HashMap<String, i32> {
  let mut counts = HashMap::new();
  for sale in &self.sales {
    *counts.entry(sale.drink.clone()).or_insert(0) += 1;
  }
  counts
}

HashMaps aggregate sales by drink name.

3. Iterator Chains

let large_latte_revenue: f64 = analytics.sales.iter()
  .filter(|sale| sale.drink == "Latte" && sale.size == "Large")
  .map(|sale| sale.price)
  .sum();

Multiple iterator operations chained together for complex queries.

4. String Manipulation

Sale {
  drink: String::from(drink),  // Convert &str to String
  ...
}

Working with owned strings for data storage.

5. Collection Methods

fn most_popular_drink(&self) -> Option<(String, i32)> {
  self.sales_by_drink()
    .into_iter()
    .max_by_key(|(_, count)| *count)
}

Using methods like max_by_key to find maximum values.

6. Sorting Collections

let mut drinks: Vec<_> = sales_by_drink.iter().collect();
drinks.sort_by_key(|(_, count)| -*count);  // Sort descending

Converting HashMaps to vectors for sorting.

Loading Exercise...

Summary

In this chapter, we explored Rust’s powerful collection types and iteration capabilities:

  • Vectors (Vec<T>) are growable arrays stored on the heap
  • Use get() for safe indexing that returns Option instead of panicking
  • HashMaps store key-value pairs and use the entry API for efficient updates
  • Strings are complex because they’re UTF-8 encoded - you can’t index them directly
  • Iterate over strings with .chars() for characters or .bytes() for bytes
  • Iterators are lazy and don’t do work until consumed
  • Iterator adapters like map and filter transform iterators
  • Iterator consumers like collect, sum, and fold produce final results
  • Peekable iterators let you look ahead at the next element without consuming it
  • Use .peekable() for parsing, grouping consecutive elements, or comparing neighbors
  • Chain iterator methods for powerful data processing pipelines

Collections and iterators are fundamental to writing efficient, expressive Rust code. The iterator approach often leads to code that’s both more readable and more performant than traditional loops.