DEV Community

Cover image for Learning Rust πŸ¦€: 16 - Rust Collections: Vectors
Fady GA 😎
Fady GA 😎

Posted on

Learning Rust πŸ¦€: 16 - Rust Collections: Vectors

Now we will have a look at some of Rust's most common Collections, The Vector, String (yes! String type is a collection of characters 😎), and Hash Map types. We will start first with Cyborg mmm... I mean, Vector 😁

⚠️ Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.

⚠️⚠️ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

⭐ I try to publish a new article every week (maybe more if the Rust gods πŸ™Œ are generous 😁) so stay tuned πŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and Twitter.

Table of Contents:

Creating new Vectors:

Before I show you how to create a new Vector, there is something you need to know. Unlike Arrays or Tuples that can hold a list of items themselves, Vectors are created in the Heap. Meaning, that they don't have to be of known length at compile time! But similar to Arrays and Tuples, they must contain elements of the same type.

We can create an empty Vector as follows:

let v: Vec<i32> = Vec::new();
Enter fullscreen mode Exit fullscreen mode

Because the compiler doesn't know the type of the elements of the newly created Vector, there type must be explicitly stated using Vec<T> (Yes, generics which we will talk about them later). So here, we are creating an empty Vector that will hold element of type i32.

Also, we can create prefilled Vectors like this:

let v = vec![1, 2, 3];
Enter fullscreen mode Exit fullscreen mode

By using the vec! macro, we are creating also a Vec<i32> Vector as i32 is the default integer type.

Vectors - like any other Rust type - are immutable by default meaning that you can't add or remove items from them. To make them mutable, we use the mut keyword.

Updating Vectors:

The Vector type has a lot of useful methods that can update its elements. push and pop are very common in such operations. Consider the following example where we create an empty Vector and add 3 elements to it then remove one:

fn main() {
    let mut v: Vec<i32> = Vec::new();
    println!("Length of v is: {}", v.len());
    println!("New length of v is: {}", v.len());
Enter fullscreen mode Exit fullscreen mode

The first print will give you Length of v is: 3 and the second one will print New length of v is: 2. push adds elements to the Vector. I like to imagine the Vector as a "stack" and push adds a new element each time at the top of the stack which means that the very first item that was added using push will be at the bottom of the stack. pop on the other hand, remove the top item from this stack.

len is used to get the Vector "length" which in this case is the elements count.

Reading Vectors elements:

You can access the Vector's elements using the square brackets "[ ]" similar to arrays (or Python's Lists and Tuples). We can also use the get method as follows:

fn main(){
    let v = vec![1, 2, 3];

    let first_element = &v[0];
    println!("First element is {first_element}");

    let second_element: Option<&i32> = v.get(1); 
    match second_element {
        Some(second) => println!("Second element is {second}"),
        None => println!("There isn't a second element!"),
Enter fullscreen mode Exit fullscreen mode

Vectors index start at 0 so &v[0] will return a reference to the first element in the Vector which in this case will be 1. get returns an Option type (remember the Option type?) as the index passed to get can be outside of the Vector boundaries, then it will return the None variant of the Option type. Otherwise, it will return Some(T). Here, get(1) should return a Some(i32) variant as the element at index 1 exists.

Now let's see what will happen if we requested an "out of bound" element:

let v2 = vec![1, 2, 3];
let val = &v2[100];
Enter fullscreen mode Exit fullscreen mode

This will compile just fine but will make the application "panic"! This behavior can be used if you want to crash the program if an out-of-bound value is entered. Now let's look at get:

let v2 = vec![1, 2, 3];
let val = v2.get(100); // Returns None
    if let None = val {
        println!("None is returned!")
Enter fullscreen mode Exit fullscreen mode

The application won't "panic" in this case and None is returned! will be printed out.

if let is another way to do pattern matching that I mentioned here

One last thing before we leave this point, do you remember that we can't have both mutable and immutable references for a type in the same scope? This applies for Vector elements as well! Let's examine the following:

let mut v = vec![1, 2, 3];
let first = &v[0];
Enter fullscreen mode Exit fullscreen mode

This should compile just fine, right? We are using an immutable reference for the first item in the Vector and then adding a new item at the end of the vector, those should be unrelated, right? No, they are related and the code won't be compiled!

Do you recall that Vectors are stored in the Heap? Which means that they can expand as they store their elements next to each other. And if the allocated memory to the Vector isn't large enough to hold all of its new elements, a new memory location will be assigned to it. Meaning that the let first = &v[0]; will be referring to an invalid location in memory if the v.push(4); expression caused the Vector to be reallocated. Therefore, prinln!("{first}"); will cause a compilation error!

Reading Vectors elements:

Similar to Arrays, we can iterate over a Vector like this:

let v = vec![1, 2, 3];
for i in &v {
Enter fullscreen mode Exit fullscreen mode

This will result in:

Enter fullscreen mode Exit fullscreen mode

We can also change the values of the Vector's elements while iterating:

let mut v2 = vec![20, 40, 60];
for i in &mut v2 {
    *i -= 20;
println!("Modified vector:");
for i in &v2 {
Enter fullscreen mode Exit fullscreen mode

This will print:

Enter fullscreen mode Exit fullscreen mode

The "*" before i in the first for is called "dereference" operator which we will talk about in future articles.

Using Enum to make a Vector contain multiple types:

Vectors are lists of elements of the same type but we can encapsulate different types with Enum in a custom type and use it with Victors! Let's see that in action:

enum Item {

let shopping_cart = vec![
println!("Items in the shopping cart:");
for item in &shopping_cart {
Enter fullscreen mode Exit fullscreen mode

When running this code, we will have the following output:

Items in the shopping cart:
Enter fullscreen mode Exit fullscreen mode

Here we are building a shopping_cart Vector. But since Vectors can only have elements of the same type, we have created the Item Enum that has only 4 variants, Apple that takes their count, Banna that also takes their count, Tape that takes its length, and finally Book that takes its title. Then, we filled our shopping_cart Vector with variants of the Item enum which its variants encapsulate other data types and are technically of the same type. This technique will work if we know all the variants that we can use. But if we don't, the Enum won't work and we will have to use Traits which we will discuss later.

Vectors are the closest thing Rust has to Python's List's. They are relatively straight forward to create and use. Next, we will revisit another Rust Collection, The String! See you then πŸ‘‹

Top comments (0)