Collections: Vectors, Strings, and HashMaps
Learn to use common collection types like Vectors (resizable arrays), Strings (UTF-8 encoded text), and HashMaps (key-value pairs).
Rust Collections: Vectors
Vectors: Resizable Arrays in Rust
Vectors are a fundamental data structure in Rust, similar to arrays but with the crucial difference of being resizable. This means you can dynamically add or remove elements from a vector after it's created, unlike arrays which have a fixed size at compile time. Vectors store elements of the same type contiguously in memory, providing efficient access and iteration.
Creating Vectors
There are several ways to create a vector in Rust:
Empty Vector with Type Annotation:
let mut my_vector: Vec<i32> = Vec::new(); // Create an empty vector that will hold i32 (integers)
This creates an empty vector that can hold `i32` (signed 32-bit integers) elements. The `mut` keyword is important; we'll need it because we plan to add to the vector. The type annotation `Vec<i32>` tells Rust what type of data the vector will hold. If you don't provide it and try to push something in, the compiler will infer the type for you.
Vector with Initial Values:
let my_vector = vec![1, 2, 3, 4, 5]; // Creates a vector with initial values
This is a concise way to initialize a vector with known values. Rust infers the type (in this case, `i32`) based on the provided values.
Vector with a Specified Capacity:
let mut my_vector: Vec<i32> = Vec::with_capacity(10); // creates a vector with a pre-allocated capacity of 10 elements.
This creates a vector that has memory allocated to store 10 elements immediately. This *doesn't* mean the vector *contains* 10 elements, rather, that it can grow up to 10 elements before it needs to reallocate memory. This can be more performant if you know approximately how many elements you'll be adding, as it reduces the number of memory reallocations.
Modifying Vectors
Vectors are mutable by default, so you can add, remove, and modify their elements:
Adding Elements (Push):
let mut my_vector: Vec<i32> = Vec::new();
my_vector.push(10); // Add 10 to the end of the vector
my_vector.push(20); // Add 20 to the end of the vector
Removing Elements (Pop):
let mut my_vector = vec![1, 2, 3];
let last_element = my_vector.pop(); // Removes and returns the last element (if any)
println!("{:?}", last_element); // Output: Some(3)
let empty_vector: Vec<i32> = Vec::new();
let nothing = empty_vector.pop();
println!("{:?}", nothing); // Output: None
The `pop()` method removes and returns the last element of the vector. It returns `Some(element)` if the vector is not empty and `None` if the vector is empty, making it important to handle the `Option` type appropriately.
Inserting Elements:
let mut my_vector = vec![1, 2, 3];
my_vector.insert(1, 4); // Inserts 4 at index 1. The element previously at that index (2) and all subsequent elements are shifted to the right.
println!("{:?}", my_vector); // Output: [1, 4, 2, 3]
The `insert()` method inserts an element at a given index, shifting the existing elements to make space. Inserting an element at an index outside the current bounds will cause a panic.
Removing Elements at an Index:
let mut my_vector = vec![1, 2, 3];
my_vector.remove(1); // Removes the element at index 1.
println!("{:?}", my_vector); // Output: [1, 3]
The `remove()` method removes the element at a given index. Removing at an out-of-bounds index will also cause a panic.
Replacing Elements:
let mut my_vector = vec![1, 2, 3];
my_vector[1] = 5; // Change the element at index 1 to 5
println!("{:?}", my_vector); // Output: [1, 5, 3]
Elements can be replaced using indexing. Note that you *must* ensure the index is within the bounds of the vector. Accessing an index outside the current bounds will cause a panic.
Iterating Over Vectors
You can iterate over vectors in several ways:
Immutable Iteration:
let my_vector = vec![1, 2, 3];
for element in &my_vector { // Iterate over a reference to each element
println!("The value is: {}", element);
}
This iterates over the vector, providing immutable references (`&`) to each element. You can read the values, but you can't modify them directly within the loop. The `&` is *crucial* here. Without it, the loop attempts to take ownership of each element, which will result in a compiler error since `my_vector` owns the data and cannot transfer ownership piecemeal.
Mutable Iteration:
let mut my_vector = vec![1, 2, 3];
for element in &mut my_vector { // Iterate over a mutable reference to each element
*element += 10; // Dereference the mutable reference to modify the value
}
println!("{:?}", my_vector); // Output: [11, 12, 13]
This iterates over the vector, providing mutable references (`&mut`) to each element. You can modify the values within the loop. Note the `*element` dereference; we need to dereference the mutable reference to access the underlying value and modify it.
Iteration with Index:
let my_vector = vec![1, 2, 3];
for (index, element) in my_vector.iter().enumerate() {
println!("Element at index {} is: {}", index, element);
}
The `enumerate()` method adds an index to each element during iteration. This is useful if you need to know the position of each element. `iter()` is called to get the values out of the vector, or this would try to move the data out of the vector into the loop which is disallowed.
Moving Values Out of Vector:
let my_vector = vec![String::from("hello"), String::from("world")];
for s in my_vector {
println!("{}", s);
}
// my_vector is now invalid because the `String` objects have moved out.
In this case, the loop takes *ownership* of each `String` inside the vector. After the loop completes, the original `my_vector` is no longer valid, as its contents have been moved. This is typically only done when you no longer need the vector itself. This is important when working with types that don't implement the `Copy` trait, such as `String`, or any custom struct.