Collections
The standard library ships growable, heap-allocated containers for the everyday cases: Vec for ordered lists, String for text, HashMap for key-value lookup. This chapter walks through each one.
For Python developers: these match list, str, and dict closely, with stricter ownership rules.
Vec: Growable Arrays
The Rust equivalent of Python's list.
Creating Vectors
fn main() {
// Empty vector with type annotation
let v: Vec<i32> = Vec::new();
// Using the vec! macro
let v = vec![1, 2, 3, 4, 5];
// With initial capacity (avoids reallocations)
let mut v: Vec<i32> = Vec::with_capacity(10);
// Filled with a default value
let v = vec![0; 10]; // Ten zeros
}
Python comparison:
# Python lists
v = [] # Empty list
v = [1, 2, 3, 4, 5] # List literal
v = [0] * 10 # Ten zeros
Adding and Removing Elements
fn main() {
let mut v = Vec::new();
// Add elements
v.push(1);
v.push(2);
v.push(3);
// Remove last element
let last = v.pop(); // Returns Option<i32>
println!("Popped: {:?}", last); // Some(3)
// Remove at index
v.insert(0, 10); // Insert 10 at index 0
let removed = v.remove(0); // Remove at index 0
println!("Removed: {}", removed); // 10
// Extend from another collection
v.extend([4, 5, 6]);
println!("{:?}", v); // [1, 2, 4, 5, 6]
// Retain only elements matching a condition
v.retain(|&x| x % 2 == 0);
println!("{:?}", v); // [2, 4, 6]
}
Python comparison:
v = []
# Add elements
v.append(1)
v.append(2)
v.append(3)
# Remove last
last = v.pop() # Returns value directly (error if empty)
# Insert/remove at index
v.insert(0, 10)
removed = v.pop(0)
# Extend
v.extend([4, 5, 6])
# Filter (creates new list in Python)
v = [x for x in v if x % 2 == 0]
Accessing Elements
fn main() {
let v = vec![1, 2, 3, 4, 5];
// Indexing - panics if out of bounds
let third = v[2];
println!("Third: {}", third);
// .get() - returns Option, no panic
match v.get(2) {
Some(val) => println!("Third: {}", val),
None => println!("No third element"),
}
// First and last
println!("First: {:?}", v.first()); // Some(1)
println!("Last: {:?}", v.last()); // Some(5)
// Length and emptiness
println!("Length: {}", v.len());
println!("Empty: {}", v.is_empty());
}
Use .get() when the index might be out of bounds. Use [] only when you're certain it isn't.
Iterating
fn main() {
let v = vec![10, 20, 30];
// Immutable iteration
for val in &v {
println!("{}", val);
}
// Mutable iteration
let mut v = vec![10, 20, 30];
for val in &mut v {
*val += 100;
}
println!("{:?}", v); // [110, 120, 130]
// Consuming iteration (moves the vector)
for val in v {
println!("{}", val);
}
// v is no longer valid here
}
Slicing Vectors
fn main() {
let v = vec![1, 2, 3, 4, 5];
let slice = &v[1..3]; // [2, 3]
println!("{:?}", slice);
// Pass slices to functions
print_slice(&v);
print_slice(&v[1..4]);
}
fn print_slice(s: &[i32]) {
println!("{:?}", s);
}
Useful Vector Methods
fn main() {
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
// Sort
v.sort();
println!("Sorted: {:?}", v); // [1, 1, 2, 3, 4, 5, 6, 9]
// Deduplicate (must be sorted first)
v.dedup();
println!("Deduped: {:?}", v); // [1, 2, 3, 4, 5, 6, 9]
// Search
println!("Contains 5: {}", v.contains(&5));
println!("Position of 4: {:?}", v.iter().position(|&x| x == 4));
// Reverse
v.reverse();
println!("Reversed: {:?}", v);
// Chunks
let v = vec![1, 2, 3, 4, 5, 6];
for chunk in v.chunks(2) {
println!("Chunk: {:?}", chunk);
}
// Chunk: [1, 2]
// Chunk: [3, 4]
// Chunk: [5, 6]
// Windows (sliding window)
for window in v.windows(3) {
println!("Window: {:?}", window);
}
// Window: [1, 2, 3]
// Window: [2, 3, 4]
// Window: [3, 4, 5]
// Window: [4, 5, 6]
}
Vectors with Enums
Store different types using an enum:
#[derive(Debug)]
enum Cell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row = vec![
Cell::Int(42),
Cell::Float(3.14),
Cell::Text(String::from("hello")),
];
for cell in &row {
match cell {
Cell::Int(n) => println!("Integer: {}", n),
Cell::Float(f) => println!("Float: {}", f),
Cell::Text(s) => println!("Text: {}", s),
}
}
}
String: UTF-8 Text
String vs &str
| Feature | String | &str |
|---|---|---|
| Ownership | Owned, heap-allocated | Borrowed reference |
| Mutability | Growable with mut | Always immutable |
| Use as parameter | When you need ownership | Default choice for read-only |
| Created from | String::from(), .to_string() | String literals, slicing |
Creating Strings
fn main() {
let s1 = String::new();
let s2 = String::from("hello");
let s3 = "hello".to_string();
let s4 = format!("{} {}", "hello", "world");
// From characters
let s5: String = vec!['h', 'e', 'l', 'l', 'o']
.into_iter()
.collect();
}
Modifying Strings
fn main() {
let mut s = String::from("hello");
// Append
s.push(' '); // Single character
s.push_str("world"); // String slice
println!("{}", s); // "hello world"
// Insert
s.insert(5, ','); // Insert char at byte index
s.insert_str(6, " beautiful");
println!("{}", s); // "hello, beautiful world"
// Replace
let s2 = s.replace("beautiful", "cruel");
println!("{}", s2); // "hello, cruel world"
// Truncate
let mut s3 = String::from("hello");
s3.truncate(3);
println!("{}", s3); // "hel"
// Clear
s3.clear();
println!("Empty: '{}'", s3);
}
Concatenation
fn main() {
let s1 = String::from("Hello");
let s2 = String::from(" World");
// Using + (moves s1)
let s3 = s1 + &s2;
// s1 is invalid now, s2 still valid
println!("{}", s3);
// Using format! (doesn't move anything)
let s4 = String::from("Hello");
let s5 = format!("{} {}", s4, s2);
println!("{}", s5); // s4 and s2 still valid
// Join a collection
let parts = vec!["hello", "world", "rust"];
let joined = parts.join(", ");
println!("{}", joined); // "hello, world, rust"
}
Iterating Over Strings
Strings in Rust are UTF-8 encoded. You can't index them by position because characters can be multiple bytes.
fn main() {
let s = String::from("héllo");
// By characters
for c in s.chars() {
print!("{} ", c); // h é l l o
}
println!();
// By bytes
for b in s.bytes() {
print!("{} ", b); // 104 195 169 108 108 111
}
println!();
// Character count vs byte length
println!("Chars: {}", s.chars().count()); // 5
println!("Bytes: {}", s.len()); // 6
// Get nth character
let third = s.chars().nth(2);
println!("Third char: {:?}", third); // Some('l')
}
String Searching and Slicing
fn main() {
let s = String::from("hello world rust");
// Search
println!("Contains 'world': {}", s.contains("world"));
println!("Starts with 'hello': {}", s.starts_with("hello"));
println!("Ends with 'rust': {}", s.ends_with("rust"));
println!("Find 'world': {:?}", s.find("world")); // Some(6)
// Split
let words: Vec<&str> = s.split_whitespace().collect();
println!("Words: {:?}", words);
let parts: Vec<&str> = "a,b,c,d".split(',').collect();
println!("Parts: {:?}", parts);
// Trim
let padded = " hello ";
println!("Trimmed: '{}'", padded.trim());
println!("Trim start: '{}'", padded.trim_start());
println!("Trim end: '{}'", padded.trim_end());
// Case conversion
println!("Upper: {}", s.to_uppercase());
println!("Lower: {}", s.to_lowercase());
}
String Conversion
fn main() {
// Number to string
let n = 42;
let s = n.to_string();
let s2 = format!("{}", n);
// String to number
let n: i32 = "42".parse().expect("Not a number");
let n: f64 = "3.14".parse().unwrap();
// Safe parsing
match "abc".parse::<i32>() {
Ok(n) => println!("Got: {}", n),
Err(e) => println!("Parse error: {}", e),
}
}
HashMap: Key-Value Pairs
Creating HashMaps
use std::collections::HashMap;
fn main() {
// Empty
let mut scores: HashMap<String, i32> = HashMap::new();
// Insert entries
scores.insert(String::from("Alice"), 100);
scores.insert(String::from("Bob"), 85);
// From iterators
let teams = vec!["Alice", "Bob"];
let initial_scores = vec![100, 85];
let scores: HashMap<_, _> = teams
.into_iter()
.zip(initial_scores.into_iter())
.collect();
// With capacity
let map: HashMap<String, i32> = HashMap::with_capacity(10);
println!("{:?}", scores);
}
Accessing Values
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
// Get returns Option<&V>
match scores.get("Alice") {
Some(score) => println!("Alice: {}", score),
None => println!("Alice not found"),
}
// Direct access with default
let alice_score = scores.get("Alice").copied().unwrap_or(0);
println!("Alice: {}", alice_score);
// Check existence
println!("Has Alice: {}", scores.contains_key("Alice"));
println!("Has Charlie: {}", scores.contains_key("Charlie"));
// Length
println!("Count: {}", scores.len());
}
Iterating
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
scores.insert("Charlie", 92);
// Iterate over key-value pairs
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// Just keys
for name in scores.keys() {
println!("Player: {}", name);
}
// Just values
let total: i32 = scores.values().sum();
println!("Total: {}", total);
}
Updating Values
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
// Insert (overwrites existing)
scores.insert("Alice", 100);
scores.insert("Alice", 200); // Overwrites
println!("Alice: {}", scores["Alice"]); // 200
// Insert only if key doesn't exist
scores.entry("Bob").or_insert(85);
scores.entry("Bob").or_insert(999); // Ignored, Bob already exists
println!("Bob: {}", scores["Bob"]); // 85
// Update based on old value
let text = "hello world hello rust hello";
let mut word_count = HashMap::new();
for word in text.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", word_count);
// {"hello": 3, "world": 1, "rust": 1}
// Remove
scores.remove("Alice");
println!("Has Alice: {}", scores.contains_key("Alice"));
}
Ownership with HashMap
use std::collections::HashMap;
fn main() {
let key = String::from("color");
let value = String::from("blue");
let mut map = HashMap::new();
map.insert(key, value);
// key and value are moved - can't use them anymore
// println!("{}", key); // ERROR: moved
// println!("{}", value); // ERROR: moved
// Use references if you want to keep ownership
let key2 = String::from("size");
let value2 = String::from("large");
let mut map2 = HashMap::new();
map2.insert(&key2, &value2); // Borrows, not moves
println!("{} = {}", key2, value2); // Still valid
}
Choosing the Right Collection
| Need | Collection | Why |
|---|---|---|
| Ordered list, grow/shrink | Vec<T> | Dynamic array, fast indexing |
| Key-value lookup | HashMap<K, V> | O(1) average lookup |
| Unique values | HashSet<T> | Deduplication, membership tests |
| Ordered key-value | BTreeMap<K, V> | Sorted keys, range queries |
| Double-ended queue | VecDeque<T> | Fast push/pop from both ends |
| Growable text | String | UTF-8 encoded text |
Iterator Methods on Collections
Most collection operations are done through iterators:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filter and collect
let evens: Vec<i32> = numbers.iter()
.filter(|&&n| n % 2 == 0)
.copied()
.collect();
println!("Evens: {:?}", evens);
// Map and collect
let squared: Vec<i32> = numbers.iter()
.map(|&n| n * n)
.collect();
println!("Squared: {:?}", squared);
// Fold (reduce)
let sum = numbers.iter().fold(0, |acc, &n| acc + n);
println!("Sum: {}", sum);
// any / all
let has_even = numbers.iter().any(|&n| n % 2 == 0);
let all_positive = numbers.iter().all(|&n| n > 0);
println!("Has even: {}, All positive: {}", has_even, all_positive);
// min / max
println!("Min: {:?}", numbers.iter().min());
println!("Max: {:?}", numbers.iter().max());
// Chaining
let result: Vec<String> = numbers.iter()
.filter(|&&n| n > 5)
.map(|n| format!("#{}", n))
.collect();
println!("Result: {:?}", result);
}
Practice Exercises
Exercise 1: Word Frequency
Count the frequency of each word in a text and print the top 3 most common:
use std::collections::HashMap;
fn word_frequency(text: &str) -> HashMap<String, usize> {
// Your code
}
Exercise 2: Student Grades
Store students and their grades in a HashMap. Calculate the class average and find the top student:
use std::collections::HashMap;
fn main() {
let mut grades: HashMap<String, Vec<f64>> = HashMap::new();
// Add students and grades
// Calculate averages
// Find top student
}
Exercise 3: Flatten Nested Vectors
Write a function that flattens a Vec<Vec<i32>> into a single Vec<i32>.
Common Mistakes
1. Indexing Out of Bounds
let v = vec![1, 2, 3];
let x = v[5]; // PANIC at runtime
Fix: Use .get(5) which returns Option<&T>.
2. Modifying While Iterating
let mut v = vec![1, 2, 3];
for val in &v {
v.push(*val); // ERROR: can't borrow as mutable
}
Fix: Collect changes into a new vector, then apply them.
3. String Indexing
let s = String::from("hello");
let c = s[0]; // ERROR: String can't be indexed
Fix: Use s.chars().nth(0) or &s[0..1] (byte range, ASCII only).
Key Takeaways
Vec<T>is the go-to growable list. Reach for.push(),.pop(), and.get()Stringis UTF-8, so indexing by position doesn't work directlyHashMap<K, V>gives fast key-value lookup- Use
.entry().or_insert()for insert-if-absent patterns - Iterators are the idiomatic way to transform collections
- Collections own their data. Inserting moves values in
Next Steps
Continue to 08-error-handling.md to learn how Rust handles things that go wrong, with Result and Option.
Quick Reference
// Vec
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();
let x = v.get(0); // Option<&i32>
for val in &v { }
// String
let mut s = String::from("hello");
s.push_str(" world");
for c in s.chars() { }
let words: Vec<&str> = s.split_whitespace().collect();
// HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key", "value");
map.get("key"); // Option<&&str>
map.entry("key").or_insert("default");
for (k, v) in &map { }