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<T>
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
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<K, V>
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);
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 alot.
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!");
}
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]
BTreeMap and BTreeSet: Ordered Alternatives
Sometimes you need your data sorted. BTreeMap<K
, V>
and BTreeSet<T>
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();
BinaryHeap: Priority Queue
Need a priority queue? BinaryHeap<T>
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
}
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()];
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
});
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
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);
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);
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
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<T>
Need efficient insertion/removal at both ends? → VecDeque<T>
Need key-value lookups without caring about order? → HashMap<K, V>
Need key-value lookups with sorted keys? → BTreeMap<K, V>
Need to track unique values without caring about order? → HashSet<T>
Need unique values in sorted order? → BTreeSet<T>
Need a priority queue? → BinaryHeap<T>
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 !!