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.
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.
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.
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 Option
s:
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.
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.
Method | Produces | Ownership effect | Common use case |
---|---|---|---|
iter | Iterator<Item = &T> | Borrows the collection (read-only) | Reading elements without consuming the collection |
iter_mut | Iterator<Item = &mut T> | Borrows the collection mutably | Modifying elements in-place while iterating |
into_iter | Iterator<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.
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.