Collections
Rust's standard library includes several useful data structures called collections. Unlike arrays and tuples, these store data on the heap and can grow or shrink at runtime.
For Python developers: These are similar to Python's built-in collections (list, dict, set), but 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 [] when you're certain.
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: use.push(),.pop(), and.get()Stringis UTF-8, so indexing by position doesn't work directlyHashMap<K, V>provides 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
In the next chapter, we'll explore error handling with Result and Option - how Rust handles things that can go wrong.
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 { }