DEV Community

Jane for Mastering Backend

Posted on • Originally published at masteringbackend.com on

Collections in Rust: The Essentials

title

When you’re building real applications in Rust, you’ll quickly discover that primitive types only get you so far. You need data structures that can grow, shrink, and organize multiple values efficiently.

That’s where Rust’s collections come in.

Collections are the workhorses of modern programming. They’re heap-allocated, carefully articulated collections of data structures that can hold multiple values and resize dynamically during runtime. Rust’s standard library provides several powerful collections, each optimized for different use cases. Let’s dive into the ones you’ll probably use in production code.

You see, rust collections are broken into four separate groups

  • Sequences: Vec, VecDeque, LinkedList
  • Maps: _HashMap, _** BTreeMap**
  • Sets: _HashSet, _** BTreeSet**
  • Misc: BinaryHeap

In this article, we will go over the most commonly used ones and basic knowledge on their usage. You would learn the basics of collections.

The Big Three: Vec, HashMap, and HashSet

Vector: Your Dynamic Array

The Vec It's probably the first collection you'll reach for. Think of it as a resizable array that lives on the heap. It's ordered, allows duplicates, and provides fast indexed access.

// Creating vectors 
let mut numbers = Vec::new(); 
let mut scores = vec![85, 92, 78, 96]; // macro for convenience 

// Adding elements 
numbers.push(42); 
numbers.push(17); 
numbers.extend([1, 2, 3]); // add multiple at once 

// Accessing elements 
let first = scores[0]; // panics if out of bounds 
let maybe_second = scores.get(1); // returns Option<&T> 

// Iterating 
for score in &scores { 
    println!("Score: {}", score); 
 } 

 // Capacity management 
 println!("Length: {}, Capacity: {}", scores.len(), scores.capacity()); 
 scores.reserve(100); // pre-allocate space
Enter fullscreen mode Exit fullscreen mode

The beauty of Vec lies in its versatility. Need a stack? Use push() and pop(). Need a queue? Use push() and remove(0) (though VecDeque is better for this). Need to sort data? Call sort() or sort_by(). You could see it as the Swiss knife of collections

HashMap: Key-Value Powerhouse

When you need to associate keys with values, HashMap is your friend. It provides average O(1) insertion, deletion, and lookup times, making it perfect for caches, indices, and lookup tables.


use std::collections::HashMap; 

// Creating and populating 
let mut student_grades = HashMap::new(); 
student_grades.insert("Alice", 95); 
student_grades.insert("Bob", 87); 
student_grades.insert("Charlie", 92); 

// Alternative creation 
let scores = HashMap::from([ 
    ("Math", 85), 
    ("Science", 90), 
    ("History", 78), 
]); 

// Accessing values 
match student_grades.get("Alice") { 
     Some(grade) => println!("Alice scored: {}", grade), 
     None => println!("Student not found"), 
 } 

 // The entry API is incredibly useful 
 let grade = student_grades.entry("David").or_insert(0); 
  *grade += 10; // David now has 10 

 // Updating existing values 
 student_grades.entry("Alice").and_modify(|g| *g += 5);
Enter fullscreen mode Exit fullscreen mode

The entry API deserves special attention. It’s one of Rust’s hashmaps most used features, allowing you to handle the “insert if missing, update if present” pattern without multiple lookups, basically you can manipulate an entry by calling a mutable instance of the entry — allowing you to do a lot.

HashSet: Unique Values Only

HashSet<T> is essentially a HashMap where you only care about the keys. It guarantees uniqueness and provides fast membership testing.


use std::collections::HashSet; 

let mut unique_ids = HashSet::new(); 
unique_ids.insert(42); 
unique_ids.insert(17); 
unique_ids.insert(42); // duplicate - won't be added 

// Set operations 
let set1: HashSet<i32> = [1, 2, 3, 4].into(); 
let set2: HashSet<i32> = [3, 4, 5, 6].into(); 

let intersection: HashSet<_> = set1.intersection(&set2).cloned().collect(); 
let union: HashSet<_> = set1.union(&set2).cloned().collect(); 
let difference: HashSet<_> = set1.difference(&set2).cloned().collect(); 

// Membership testing 
if unique_ids.contains(&42) { 
    println!("Found it!"); }
Enter fullscreen mode Exit fullscreen mode

The Supporting Cast

VecDeque: Double-Ended Queue

When you need efficient insertion and removal at both ends, VecDeque<T > (pronounced "vec-deck") is your answer. It's implemented as a ring buffer.


use std::collections::VecDeque; 

let mut queue = VecDeque::new(); 

// Add to both ends 
queue.push_back(1); // [1] 
queue.push_front(0); // [0, 1] 
queue.push_back(2); // [0, 1, 2] 

// Remove from both ends 
let front = queue.pop_front(); // Some(0), queue is now [1, 2] 
let back = queue.pop_back(); // Some(2), queue is now [1]
Enter fullscreen mode Exit fullscreen mode

BTreeMap and BTreeSet: Ordered Alternatives

Sometimes you need your data sorted. BTreeMap<K , V> and BTreeSet maintain their elements in sorted order, providing O(log n) operations.


use std::collections::BTreeMap; 

let mut leaderboard = BTreeMap::new(); 
leaderboard.insert("Alice", 1500); 
leaderboard.insert("Bob", 1200); 
leaderboard.insert("Charlie", 1800); 

// Iteration is automatically sorted by key 
for (name, score) in &leaderboard { 
     println!("{}: {}", name, score); // Alphabetical order 
} 

// Range queries 
let high_scorers: BTreeMap<_, _> = leaderboard 
    .range("Bob".."Charlie") 
    .map(|(k, v)| (*k, *v)) 
    .collect();
Enter fullscreen mode Exit fullscreen mode

BinaryHeap: Priority Queue

Need a priority queue? BinaryHeap implements a max-heap, always keeping the largest element at the front.


use std::collections::BinaryHeap; 

let mut heap = BinaryHeap::new(); 
heap.push(5); 
heap.push(1); 
heap.push(9); 
heap.push(3); 

while let Some(max) = heap.pop() { 
    println!("{}", max); // Prints: 9, 5, 3, 1 
}
Enter fullscreen mode Exit fullscreen mode

Performance Characteristics: Choose Wisely

Knowing what to choose is very dicey since it largely depends on your use case , however here are some general guides to help you pick.

Vec : Use when you need indexed access, don’t mind occasional expensive resizing, and primarily append to the end. Perfect for most use cases.

VecDeque : Choose when you need efficient operations at both ends. Great for queues and sliding windows.

HashMap : Your go-to for key-value associations when you don’t need ordering. Excellent for caches and lookup tables.

BTreeMap : Use when you need sorted key-value pairs or range queries. Slightly slower than HashMap but maintains order.

HashSet : Perfect for membership testing and eliminating duplicates.

BTreeSet : When you need a sorted set or want to perform set operations on ordered data.

BinaryHeap : Essential for priority queues and algorithms that need “give me the next largest/smallest” semantics.

Memory Management and Ownership

Collections in Rust follow the same ownership rules as everything else, but they introduce some interesting patterns:


let mut data = vec![String::from("hello"), String::from("world")]; 

// This moves the string out of the vector 
let first = data.remove(0); // "hello" is now owned by `first` 

// This borrows without moving 
let second = &data[0]; // borrowing "world" 

// Collections can own or borrow 
let borrowed_data: Vec<&str> = vec!["hello", "world"]; 
let owned_data: Vec<String> = vec!["hello".to_string(), "world".to_string()];
Enter fullscreen mode Exit fullscreen mode

When designing your data structures, consider whether your collection should own its data or just hold references. Owned data is simpler but uses more memory. Borrowed data is efficient but comes with lifetime constraints.

Common Patterns and Idioms

The Collect Pattern

One of Rust’s most powerful patterns is using iterators with collect():


// Transform and collect 
let numbers = vec![1, 2, 3, 4, 5]; 
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); 

// Filter and collect 
let evens: Vec<i32> = numbers.into_iter().filter(|&x| x % 2 == 0).collect(); 

// Collect into different types 
let unique_numbers: HashSet<i32> = vec![1, 2, 2, 3, 3, 4].into_iter().collect(); 
let letter_counts: HashMap<char, usize> = "hello" 
    .chars() 
    .fold(HashMap::new(), |mut acc, c| { 
         *acc.entry(c).or_insert(0) += 1; 
          acc 
     });
Enter fullscreen mode Exit fullscreen mode

Building Collections from Other Collections


// Converting between types 
let vec_data = vec![1, 2, 3]; 
let set_data: HashSet<_> = vec_data.into_iter().collect(); 
let vec_again: Vec<_> = set_data.into_iter().collect(); 

// Partial moves 
let original = vec![1, 2, 3, 4, 5]; 
let (first_half, second_half) = original.split_at(3); 
let first_vec = first_half.to_vec(); // cloning is explicit
Enter fullscreen mode Exit fullscreen mode

Custom Hashing and Ordering

Sometimes the default behavior isn’t what you want:

use std::collections::HashMap; 
use std::hash::{Hash, Hasher}; 

#[derive(Debug)] 
struct CaseInsensitiveString(String); 

impl Hash for CaseInsensitiveString { 
     fn hash<H: Hasher>(&self, state: &mut H) { 
         self.0.to_lowercase().hash(state); 
     } 
  } 

impl PartialEq for CaseInsensitiveString { 
     fn eq(&self, other: &Self) -> bool { 
         self.0.to_lowercase() == other.0.to_lowercase() 
      } 
 } 

 impl Eq for CaseInsensitiveString {} 

 // Now you can use it as a HashMap key with case-insensitive matching 
 let mut case_insensitive_map = HashMap::new(); 
 case_insensitive_map.insert(CaseInsensitiveString("Hello".to_string()), 42);
Enter fullscreen mode Exit fullscreen mode

Performance Tips and Gotchas

Pre-allocate when possible : If you know roughly how much data you’ll store, use Vec::with_capacity() or HashMap::with_capacity() to avoid reallocations, this is just how vectors in programming work.


let mut big_vec = Vec::with_capacity(1000); // avoid reallocation 
let mut cache = HashMap::with_capacity(50);
Enter fullscreen mode Exit fullscreen mode

Choose the right iteration method :

for item in collection - moves/consumes the collection 
for item in &collection - borrows each item 
for item in &mut collection - mutably borrows each item
Enter fullscreen mode Exit fullscreen mode

HashMap keys must implement Hash + Eq : Most built-in types work, but be careful to ensure it is done how you expect especially for custom types.

Vec indexing can panic : Use get() for safe access, especially with user input.

When to Use What: Decision Tree

Need ordered data with indexed access?Vec

Need efficient insertion/removal at both ends?VecDeque

Need key-value lookups without caring about order?HashMap

Need key-value lookups with sorted keys?BTreeMap

Need to track unique values without caring about order?HashSet

Need unique values in sorted order?BTreeSet

Need a priority queue?BinaryHeap

The Bottom Line

Rust’s collections are designed with performance and memory safety in mind. They give you the tools to build efficient, correct programs without the fear of memory leaks or buffer overflows that plague other systems languages.

The core point here is that rust collections are just normal structures like you could write , they simply come with the standard library to ease you writing them.

Remember: premature optimization is still the root of all evil, even in Rust. Start with the simplest collection that meets your needs (Vec for most sequences, HashMap for most key-value pairs), and only switch to more specialized collections when profiling shows they're necessary. The Rust standard library's collections are all highly optimized, so your choice of collection often matters more than micro-optimizations within a collection type.

If you have any questions, feel free to reach out to me on my LinkedIn. For more Rust content, see https://rustdaily.com/

Goodbye, see you next week !!

Have a great one!!!

Author: by Ugochukwu Chizaram


Thank you for being a part of the community

Before you go:

Whenever you’re ready

There are 4 ways we can help you become a great backend engineer:

  • The MB Platform: Join thousands of backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.
  • The MB Academy: The “MB Academy” is a 6-month intensive Advanced Backend Engineering Boot Camp to produce great backend engineers.
  • Join Backend Weekly: If you like posts like this, you will absolutely enjoy our exclusive weekly newsletter, sharing exclusive backend engineering resources to help you become a great Backend Engineer.
  • Get Backend Jobs: Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board.

Originally published at https://masteringbackend.com on September 3, 2025.


Top comments (0)