On this page
Collections: Vec, String, and HashMap
Collections in Rust
Rust provides several collections in its standard library, all located in std::collections. The three most important are Vec<T>, String, and HashMap<K, V>.
Vec: the dynamic array
Vec<T> is the most-used collection in Rust. It is a dynamically-sized array that stores elements of the same type on the heap:
fn main() {
// Create an empty Vec — you need to specify the type
let mut v1: Vec<i32> = Vec::new();
// With the vec! macro
let v2 = vec![1, 2, 3, 4, 5];
// With initial capacity (avoids reallocations)
let mut v3: Vec<String> = Vec::with_capacity(10);
// Add elements
v1.push(10);
v1.push(20);
v1.push(30);
// Remove and return the last one
let last = v1.pop(); // Some(30)
println!("{:?}", last);
// Insert at a specific position
v1.insert(0, 5); // Insert 5 at the beginning
// Remove by index
let removed = v1.remove(0); // Removes and returns element at index 0
println!("Removed: {removed}");
println!("v1: {:?}", v1);
println!("v2: {:?}", v2);
}Accessing elements
fn main() {
let v = vec![10, 20, 30, 40, 50];
// Index access — can panic if out of bounds
let third = v[2];
println!("Third: {third}");
// Safe access with get() — returns Option<&T>
match v.get(100) {
Some(val) => println!("Value: {val}"),
None => println!("Index out of bounds"),
}
// Vec slices
let slice: &[i32] = &v[1..4]; // [20, 30, 40]
println!("Slice: {:?}", slice);
// first() and last()
println!("First: {:?}", v.first());
println!("Last: {:?}", v.last());
}Iterating over Vec
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Iterate with reference (does not consume the Vec)
for n in &numbers {
print!("{n} ");
}
println!();
// Iterate with mutable reference
for n in &mut numbers {
*n *= 2;
}
println!("{:?}", numbers);
// Iterate by value (consumes the Vec)
let squares: Vec<i32> = numbers.into_iter().map(|n| n * n).collect();
println!("{:?}", squares);
// Filter
let evens: Vec<i32> = squares.iter().filter(|&&n| n % 2 == 0).cloned().collect();
println!("Evens: {:?}", evens);
// enumerate
for (i, val) in evens.iter().enumerate() {
println!("[{i}] = {val}");
}
}Common Vec operations
fn main() {
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
println!("Length: {}", v.len());
println!("Is empty? {}", v.is_empty());
println!("Contains 5? {}", v.contains(&5));
// Sort
v.sort();
println!("Sorted: {:?}", v);
// Deduplicate (requires sorted first)
v.dedup();
println!("Without duplicates: {:?}", v);
// Reverse
v.reverse();
println!("Reversed: {:?}", v);
// Truncate
v.truncate(5);
println!("Truncated to 5: {:?}", v);
// retain: keep only elements that satisfy condition
v.retain(|&n| n > 2);
println!("Only > 2: {:?}", v);
// extend: add elements from another iterable
v.extend([10, 20, 30]);
println!("Extended: {:?}", v);
// split_at
let (left, right) = v.split_at(3);
println!("Left: {:?}, Right: {:?}", left, right);
}String: owned UTF-8 text
String is a UTF-8 byte vector with ownership. Unlike &str, a String can grow and be modified:
fn main() {
// Create
let mut s = String::new();
let s2 = String::from("Hello");
let s3 = "world".to_string();
// Append text
s.push_str("Welcome to Rust!");
s.push('!'); // A single char
// Concatenation with +
// Note: s2 is moved (the + operator takes ownership of the first argument)
let s4 = s2 + " " + &s3;
println!("{s4}");
// format!: concatenate without moving
let part1 = String::from("a");
let part2 = String::from("b");
let part3 = String::from("c");
let joined = format!("{part1}-{part2}-{part3}"); // None are moved
println!("{joined}");
// Useful methods
let text = String::from(" Hello, Rust! ");
println!("Trim: '{}'", text.trim());
println!("Uppercase: {}", text.to_uppercase());
println!("Lowercase: {}", text.to_lowercase());
println!("Replace: {}", text.replace("Rust", "World"));
// Split
let csv = "one,two,three,four";
let parts: Vec<&str> = csv.split(',').collect();
println!("{:?}", parts);
// Check
println!("Contains 'Rust'? {}", text.contains("Rust"));
println!("Starts with ' Hello'? {}", text.starts_with(" Hello"));
// Length — in bytes, not characters
let emoji = "🦀";
println!("Bytes of '{}': {}", emoji, emoji.len()); // 4
println!("Chars of '{}': {}", emoji, emoji.chars().count()); // 1
}HashMap: the key-value map
HashMap<K, V> associates keys of type K with values of type V. Keys must implement Eq and Hash:
use std::collections::HashMap;
fn main() {
let mut population: HashMap<&str, u64> = HashMap::new();
population.insert("Argentina", 46_000_000);
population.insert("Bolivia", 12_000_000);
population.insert("Chile", 19_500_000);
population.insert("Colombia", 51_000_000);
// Look up
println!("{:?}", population.get("Bolivia")); // Some(12000000)
println!("{}", population["Argentina"]); // 46000000 (can panic)
// contains_key
println!("Has Peru? {}", population.contains_key("Peru"));
// Remove
let removed = population.remove("Chile");
println!("Removed: {:?}", removed);
// Iterate (order not guaranteed)
let mut countries: Vec<&&str> = population.keys().collect();
countries.sort();
for country in countries {
println!("{}: {}", country, population[country]);
}
// Length
println!("Total countries: {}", population.len());
}The entry API
The entry API is the idiomatic way to work with values that may or may not exist:
use std::collections::HashMap;
fn count_words(text: &str) -> HashMap<&str, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
// If key doesn't exist, insert 0; then increment
let count = counts.entry(word).or_insert(0);
*count += 1;
}
counts
}
fn main() {
let text = "the cat ate the mouse the cat sleeps";
let counts = count_words(text);
// Sort for deterministic output
let mut words: Vec<(&&str, &usize)> = counts.iter().collect();
words.sort_by_key(|&(w, _)| *w);
for (word, n) in words {
println!("{word}: {n}");
}
// or_insert_with: compute default only if needed
let mut cache: HashMap<String, Vec<i32>> = HashMap::new();
cache.entry(String::from("key")).or_insert_with(Vec::new).push(42);
cache.entry(String::from("key")).or_insert_with(Vec::new).push(99);
println!("{:?}", cache);
}Creating HashMaps from iterators
use std::collections::HashMap;
fn main() {
// From two parallel vectors
let keys = vec!["one", "two", "three"];
let values = vec![1, 2, 3];
let map: HashMap<&str, i32> = keys.into_iter().zip(values).collect();
println!("{:?}", map);
// From a Vec of tuples
let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
let map2: HashMap<&str, i32> = pairs.into_iter().collect();
println!("{:?}", map2);
}With mastery of the main collections, you are ready to explore traits — Rust's system of abstractions that enables polymorphism without inheritance.
collect() with turbofish
When Rust cannot infer the type of collect(), use turbofish: iterator.collect::<Vec<_>>() or annotate the variable type. The _ inside Vec<_> tells Rust to infer the element type.
entry API for counting
The pattern scores.entry(key).or_insert(0) is idiomatic for counting frequencies. The or_insert method returns a mutable reference to the value (new or existing), which you can then increment: *count += 1;
use std::collections::HashMap;
fn main() {
// === Vec<T> ===
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// vec! macro for initialization
let fruits = vec!["apple", "banana", "cherry"];
// Iteration
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
// === HashMap ===
let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 87);
// entry API: insert only if absent
scores.entry(String::from("Alice")).or_insert(100);
scores.entry(String::from("Carol")).or_insert(78);
for (name, score) in &scores {
println!("{name}: {score}");
}
// Look up
if let Some(score) = scores.get("Alice") {
println!("Alice's score: {score}");
}
}
Sign in to track your progress