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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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:
Vec, VecDeque, LinkedListHashMap, BTreeMapHashSet, BTreeSetBinaryHeapThe 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.
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.
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.
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);
}
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.
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);
}
}
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),
];
...
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.
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();
}
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);
}
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.
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.
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);
...
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.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Rust apps — start monitoring for free.

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now