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
- Updating Vectors
- Reading Vectors elements
- Using Enum to make a Vector contain multiple types
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();
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];
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();
v.push(1);
v.push(2);
v.push(3);
println!("Length of v is: {}", v.len());
v.pop();
println!("New length of v is: {}", v.len());
}
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!"),
}
}
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];
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!")
}
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];
v.push(4);
println!("{first}");
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 {
println!("{i}");
}
This will result in:
1
2
3
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 {
println!("{i}")
}
This will print:
0
20
40
The "*" before
i
in the firstfor
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:
#[derive(Debug)]
enum Item {
Apple(u8),
Banana(u8),
Tape(f32),
Book(String),
}
let shopping_cart = vec![
Item::Apple(10),
Item::Banana(5),
Item::Book(String::from("Moby-Dick")),
Item::Tape(5.5),
];
println!("Items in the shopping cart:");
for item in &shopping_cart {
println!("{item:?}");
}
When running this code, we will have the following output:
Items in the shopping cart:
Apple(10)
Banana(5)
Book("Moby-Dick")
Tape(5.5)
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)