Safety and Efficiency with Rust

Collections and Iteration


Learning Objectives

  • You know how to create and manipulate vectors and hash maps in Rust.
  • You know how to iterate over collections in Rust, and know how to manipulate elements while iterating.

There are two main data structures that are used to store collections of data in Rust: vectors and maps. Vectors are used to store a sequence of items of the same type, while maps are used to store key-value pairs. Here, we first look into vectors and maps, and then we will see how to iterate over them.

Vectors

A vector Vec<T> is a growable sequence of items of the same type, similar to what many other languages call a list. You can add or remove elements at will, and it automatically adjusts its size as needed.

Creating a vector

There are two ways to create a vector. You can either call the Vec::new() function, which creates an empty vector, or use the vec! macro, which can directly create and initialize a vector with elements.

If you see code like [1, 2, 3] in Rust, it’s an array — a sequence of items with a fixed size. Arrays can be converted into vectors using .to_vec().

Accessing elements

There are two ways to access elements of a vector. You can use the square brackets [] with the index of the element you want to access, or the get method, which returns an Option<&T>.

Adding and removing elements

You can grow a vector by pushing and appending elements:

  • push(item) adds one item to the end.
  • append(&mut another_vec) moves all elements from one vector to another.

And reduce the size of a vector by popping and removing elements:

  • pop() removes and returns the last item (if any).
  • remove(index) removes and returns the item at a specific position.

Loading Exercise...

Ownership and cloning

Like many data types in Rust (such as String), a vector is not copied automatically when you pass it around. Instead, it moves unless you explicitly clone it or pass it by reference:

Look for more vector methods in the standard library documentation.

Loading Exercise...

Maps

A map (sometimes called a dictionary in other languages) is a data structure that associates a unique key with a value. Rust provides multiple map implementations in the standard library. The most commonly used one is HashMap.

Creating a HashMap

To use a HashMap, first bring it into scope with use std::collections::HashMap;, which can be read as “use the HashMap from the collections module in the standard library”. Creating an empty HashMap is similar to creating an empty vector (there’s no macro though):

Here, each key is a String representing an English word, and each value is another String representing its Chinese counterpart.

To create a HashMap more concisely, you can use HashMap::from, which takes an array of key-value pairs:

Above, the key-value pairs in the array are tuples. A tuple is a fixed-size collection of values, similar to an array, but tuples can hold different types of values.

Loading Exercise...

Accessing and removing entries

Similarly to vectors, you can access values in a HashMap unsafely with square brackets [] or safely with the get method — by safe, we mean that it returns an Option:

Values can be removed using the remove method, which returns the value associated with the key, and the remove_entry method, which returns the key-value pair as a tuple. Both are returned as Options:


Loading Exercise...

Updating values

Modifying values is done either with the method get_mut that returns a mutable reference to the value, or with the entry method that returns an Entry enum. The Entry enum has methods like or_insert to modify the value in place.

The get_mut method returns an Option<&mut V>, where V is the type of the values in the HashMap. If the key is not in the HashMap, it returns None.

Another way to access a value is to use the entry method, which returns an Entry enum that can be either Occupied (with value) or Vacant (without value).

The or_insert method of Entry allows us to safely retrieve the value from the entry. If the entry is empty, it will insert the provided value and return a mutable reference to it. If the entry is occupied, it will return a mutable reference to the value inside.

The mutable reference allows easy modification of the value.


Loading Exercise...

Matching on Entry

As an Entry can be either Occupied or Vacant, it can also be handled with a match expression. To use the Entry enum, you need to import it separately from the HashMap:

There are some caveats in the above example code. We use the owned String instead of the borrowed &str for the keys in the hash map because we want to avoid lifetime collisions of references. We will look into other methods later in the course. Also, the prices parameter needs to be mutable even when we don’t modify the hash map because the entry method returns a mutable reference from the hash map.

Iterating over collections

We earlier used a for loop to iterate over a range of numbers. The for loop took an iterator and executed the loop body for each element in the iterator.

We can also use the for loop for iterating over collections like vectors and hash maps, as they implement the Iterator trait. Rust provides three main ways to iterate over a collection: borrowing immutably, borrowing mutably, and taking ownership of each element. The method you choose depends on what you want to do with the elements.

MethodProducesOwnership effectCommon use case
iterIterator<Item = &T>Borrows the collection (read-only)Reading elements without consuming the collection
iter_mutIterator<Item = &mut T>Borrows the collection mutablyModifying elements in-place while iterating
into_iterIterator<Item = T>Consumes the collection (moves out)Transforming or taking ownership of items, after which the collection is unusable

Iterating over a vector

To iterate over a vector with a for loop, we borrow a vector and use the in keyword to iterate over the vector elements. As the vector is borrowed, it is still available for use after the loop.

The &numbers implicitly calls the iter method of the vector, which returns an iterator over the elements of the vector. The iter method returns an iterator that borrows the vector, so the vector is still available after the loop.

If we would wish to modify the vector while iterating, we can use the iter_mut method, which returns a mutable iterator over the elements of the vector.


Loading Exercise...

Iterating over a HashMap

When iterating over a HashMap, we iterate over the entries.

Again, similarly to vectors, we can use the iter_mut method to get a mutable iterator over the entries of the HashMap.


Loading Exercise...