Ikeh Akinyemi Ikeh Akinyemi is a software engineer based in Rivers State, Nigeria. He’s passionate about learning pure and applied mathematics concepts, open source, and software engineering.

Working with Rust collections

6 min read 1708

Rust Logo

Rust offers a variety of collection types to provide a means to store and retrieve data efficiently. Each type has different characteristics when it comes to performance and capacity. Collections allows you to store multiple values sequentially but unlike arrays or tuples, collections are allocated on the heap meaning the size of the collection could grow or shrink as the program runs.

In this article, we’ll provide an overview of Rust collections, specifically the most common Rust collections types: vectors, strings, and hash maps. We’ll also discuss which collections are most appropriate for different tasks and how to efficiently use each collection.

Jump ahead:

What are Rust collections?

Collections are data structures that are provided by Rust’s standard library. These structures store information in sequences or groups. While most other Rust data types contain only one particular value, collections may contain multiple values at a time.

Rust collections can be grouped into four categories:

  • Sequence: Vec, VecDeque, LinkedList
  • Maps: HashMap, BTreeMap
  • Sets: HashSet, BTreeSet
  • Misc: BinaryHeap

Which Rust collection should you use?

The art of choosing the best collection for a given task is essential since each collection has different characteristics when it comes to its usage, performance, and capacity. Most collections provide a capacity method to query the actual capacity, or space allocation, of the collection. It is most efficient when the collection has the appropriate capacity to size the elements that are added to it.

The Vec (vector) and HashMap collections cover most use cases for generic data storage and processing. The vector is the default choice for storing items together. We can use vectors when we want a resizable array, a heap-allocated array, or a sequence of elements in a particular order.

HashMap is optimal for use cases where we want to associate random keys with random values, a map without additional functionality, or a cache.

BinaryHeap (a binary tree) is useful when we want a priority queue or when we’re looking to process only the most important or largest element each time.

There’re many other use cases for different collections in Rust, and this guide can serve as a reference to learn more.

We made a custom demo for .
No really. Click here to check it out.

Rust’s standard library includes a number of rich collections for storing values. Let’s take a closer look at the three of the most commonly used Rust collections: vectors, strings, and hash maps.

Vectors

A vector is a global continual stack of items of the same type that allows us to store multiple values next to each other, just like an array. In fact, vectors are resizable arrays, meaning they can increase or decrease in size dynamically during runtime.

Note that vectors, just like any other type stored on the heap, will be dropped when they go out of scope or are no longer used by the program.

Creating a vector

The Vec::new() function is used to create an empty vector.

In the below code, we declare and initialize the v variable as a vector type by calling the new function on the vector type. This creates an empty vector. Because we haven’t yet populated the vectors with any values, Rust can’t infer what type of value we want the vector to contain. Therefore, we have to manually annotate the type using 64-bit integer, i64, generic syntax.

fn main() {
  let v : Vec<i64> = Vec::new();
}

In order to add elements to a vector variable, we make the vector mutable by using the mut keyword. Then, we call the push() method to push elements into the vector.

fn main() {
  let mut v : Vec<i64> = Vec::new();
  v.push(1);
  v.push(2);
}

Accessing elements in a vector

There are two ways to access elements in a vector. We can directly reference an index in the vector, or we can use the get() method.

In the below snippet, we create a variable second that is equal to &v. This specifies a reference to the vector, and then we specify the desired index in square brackets. We pass 1 in the square bracket to get the second element in the vector because, just like arrays, vectors are zero indexed.

fn main() {
  let v: Vec<i64> = vec![1, 2, 3, 4];
  let second = &v[1];
  println!(“the second number is {}”, second);
}

In the above example, we’ll get an error if we pass an index that exceeds the length of the vector.

The get() method is a safer approach in terms of handling index-out-of-bound errors:

fn main() { 
  let v: Vec<i64> = vec![1, 2, 3, 4]
  match v.get(index: 12) {
    Some(second: &i64) => println!(“The second number is {}”, second),
    None => println!(“Out of bound index”),
  }
}

In the above Some case, we store the value in a variable called second and print it to the standard output. In the None case, we print the "Out of bound index" message. The None case will only be triggered if we pass an index that is greater than the length of the vector.

Iterating over elements in a vector

We can use the for in loop to iterate over elements in a vector.

fn main(){
  let v: Vec<i64> = vec![1, 2, 3, 4];
  for i in &v {
    println!("{}", i);
  }
}

Storing enum variance in vectors

As stated earlier in this article, vectors only store values of the same type. However, we can store elements of different types in a vector by making use of the enum variant.

...
enum MyVector {
  Int(i32),
  Float(f64),
  Text(String),
};

let row = vec![
  MyVector::Int(20),
  MyVector::Text(String::from("This is a string")),
  MyVector::Float(15.12),
];
...

Strings

In Rust, strings are stored as a collection of UTF-8 encoded bytes, and are basically just a secondary type for the vector, Vec, type.

A program needs to interpret these values in order for the computer to print out the correct characters. This is where encoding comes in. UTF-8 is a variable-width character encoding for Unicode, supporting strings ranging from one to four bytes.

There is only one string type in the Rust core language; the string slice. This is generally seen in its borrowed form, &str.

Creating a string

We can create an empty string using the new function. We can also use the string slice, &str, to create a string of characters and then use the to_string() method to convert the string slice into an owned string.

fn main() {
  let mut myString: String = string::new();
  let myString2: &str = “This is a string”;
  let myString3: String = myString2.to_string();
}

Concatenating strings

The push_str() method can be used to add elements to the end of a string.

Here we build a string that contains "John" and use the push_str method to append "Doe" to the string:

fn main() {
  let mut myString = String::from(“John”);
  myString.push_str(string: “Doe”);
}

Now, when we run our code, myString will return “John Doe”. We can also concatenate strings using the + operator. However, with this technique, it’s important to maintain type consistency among the variables.

In the below code, we move the ownership of s1 into s3. Then, we concatenate a copy of the characters of s2 to the end of s3. This saves a bit of memory, and the implementation is more efficient than copying both strings and then creating a new string with the copies. Because we’ve transferred ownership of s1, we can no longer use the s1 element.

fn main() {
  let s1: string = String::from(“John”);
  let s2: string = String::(“Doe”);
  let s3: string = s1 + &s2;
  println!("{}", s3);
}
// "John Doe"

We can also concatenate strings using the format macro. Unlike the + operator, the format macro doesn’t transfer ownership of the strings.

fn main() {
  let s1: string = String::from(“John”);
  let s2: string = String::(“Doe”);
  let s3: string = format!("{}{}", s1,s2);
}

Hash maps

The hash map is an associative container that allows us to store data in key value pairs called entries. In hash maps, no two entries can have the same key.

Creating a new hash map

To create a new hash map, we need to bring the hash map into scope from the standard library. Also, just like with vectors and strings, we can use the new function to create an empty hash map and then insert values into the hash map using the insert function.

use std::collections::HashMap;

fn main() {
  let john = String::from("John");
  let peter = String::from("Peter");

  let mut person: HashMap<String, i32> = HashMap::new();

  person.insert(john, 40);
  person.insert(peter, 20); 
}

In the above code, passing in john and peter in the insert() function will transfer ownership of the strings into the HashMap.

Accessing values in a hash map

We can access values in a hash map using the get() method. The get() method takes a reference to a key and returns an optional value. We get the Option enum because we can’t guarantee that a value will be returned.

...
  let person_name = String::from("John");
  let new_person: &i32 = if let Some(age) = person.get(&person_name) {
      age
  } else {
      &0i32
  };

    println!("{}", new_person);
...

Conclusion

Rust collections are data structures that allow us to store and retrieve data in a sequence. Collections are stored on the heap which means we can grow or shrink the data within them at runtime.

In this article, we investigated Rust collections and shared some examples and use cases for specific collections. We discussed the commonly used collections like vectors, strings, and hash maps. Vectors and hash maps cover most use cases for generic data storage and processing.

Rust collections are very useful for essential operations such as caching using hash maps, mapping and sorting values by their keys using BTreeMap, and storing and processing values using arrays. Rust collections can also serve as a form of local storage to store and retrieve data in programs and processes that are not heavy duty.

To learn more about Rust collections, refer to the official docs.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Ikeh Akinyemi Ikeh Akinyemi is a software engineer based in Rivers State, Nigeria. He’s passionate about learning pure and applied mathematics concepts, open source, and software engineering.

Leave a Reply