HOW-TO: Rust Collections (Vec, String, HashMap)
Master Rust's three most common collections: Vec for lists, String for text, HashMap for key-value data. Learn how ownership works with collections, when to use each, and practical patterns for real programs.
HOW-TO: Rust Collections (Vec, String, HashMap)
Overview
Collections are data structures that store multiple values. Unlike fixed-size arrays or tuples, collections grow and shrink at runtime.
Rust provides many collections in the standard library. This guide covers the three most common:
- Vec<T> — A vector (dynamic array). Store multiple values of the same type in a growable list.
- String — A growable, mutable text type. (Different from
&str, which we'll compare.) - HashMap<K, V> — Key-value storage. Look up values by key instead of index.
All three store their data on the heap, which is why they can grow. They all respect Rust's ownership rules, which means understanding Howto Rust Ownership Borrowing helps tremendously.
Warning: Collections are where ownership gets real. You'll see practical examples of moves, borrows, and mutability. Take your time with this material.
Vec<T>: Vectors
A vector is a growable array. It stores multiple values of the same type in a contiguous block of heap memory.
Use a vector when you need a list of items of uniform type.
Creating Vectors
Empty vector with type annotation:
fn main() {
let v: Vec<i32> = Vec::new();
// Empty vector, type must be specified (compiler can't infer)
}
Vector with initial values (type inferred):
fn main() {
let v = vec![1, 2, 3, 4, 5];
// Compiler infers Vec<i32> from the values
let v = vec!["hello", "world"];
// Compiler infers Vec<&str>
}
The vec! macro is more common because it's cleaner and the type is obvious.
Adding Elements (push)
fn main() {
let mut v = Vec::new(); // Must be mut to modify
v.push(5);
v.push(6);
v.push(7);
println!("{:?}", v); // [5, 6, 7]
}
Accessing Elements
Method 1: Indexing with []
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third = &v[2]; // Reference to element at index 2
println!("{third}"); // 3
let sixth = &v[5]; // PANIC! Index out of bounds
}
If the index is invalid, the program panics (crashes). Use this when you're confident the index is valid.
Method 2: get() with Option
fn main() {
let v = vec![1, 2, 3, 4, 5];
match v.get(2) {
Some(third) => println!("Third: {third}"),
None => println!("No element at index 2"),
}
match v.get(100) {
Some(element) => println!("{element}"),
None => println!("Index 100 doesn't exist"), // This prints
}
}
get() returns Option<&T>. If the index is invalid, you get None. Use this when access might fail.
Iteration
Immutable iteration (read-only):
fn main() {
let v = vec![100, 32, 57];
for element in &v {
println!("{element}");
}
}
The &v borrows the vector. You can iterate multiple times:
let v = vec![1, 2, 3];
for element in &v { println!("{element}"); }
for element in &v { println!("{element}"); } // OK, still valid
Mutable iteration (modify elements):
fn main() {
let mut v = vec![100, 32, 57];
for element in &mut v {
*element += 50; // Dereference to modify
}
println!("{:?}", v); // [150, 82, 107]
}
You dereference the mutable reference (*element) to change the value. This is the only way to modify through a mutable reference.
Ownership and Collections
This is critical. Remember Howto Rust Ownership Borrowing?
Moving into a vector:
fn main() {
let s1 = String::from("hello");
let mut v = vec![s1]; // Ownership moves to vector
// s1 is no longer valid
println!("{s1}"); // ERROR: value used after move
}
String is owned. When you push it into a vector, ownership transfers to the vector. You can't use s1 anymore.
Borrowing from a vector:
fn main() {
let mut v = vec![String::from("hello"), String::from("world")];
let first = &v[0];
println!("{first}"); // OK, borrowed
v.push(String::from("!")); // ERROR: can't modify while borrowed
}
While you hold a reference to first, you can't mutate the vector. This prevents the reference from becoming invalid (the vector might reallocate if it grows).
Best practice: Drop the borrow when done:
fn main() {
let mut v = vec![String::from("hello"), String::from("world")];
let first = &v[0];
println!("{first}"); // Borrow ends after this line
v.push(String::from("!")); // OK, no active borrow
}
Dropping a Vector
When a vector goes out of scope, it's automatically dropped — including all its elements:
fn main() {
{
let v = vec![1, 2, 3];
// use v
} // v dropped here, heap memory freed
}
String and &str
This is confusing for many. String and &str are different types.
&str: String Slices (Immutable, Fixed-Size)
A string slice is a reference to a string. It's immutable and fixed-size.
fn main() {
let s = "hello"; // &str — string literal, hardcoded
println!("{s}");
}
String literals are &str. The text is baked into your binary. They live as long as the program runs ('static lifetime).
You can't modify a &str:
let s = "hello";
s.push_str(" world"); // ERROR: &str doesn't have push_str
String: Owned, Mutable, Growable
A String is a vector of bytes that holds UTF-8 text. It's mutable and growable.
fn main() {
let s = String::from("hello"); // Owned String
// or
let s = "hello".to_string(); // Convert &str to String
}
You can modify a String:
fn main() {
let mut s = String::from("hello");
s.push_str(", world!");
println!("{s}"); // "hello, world!"
s.push('!'); // Add a single character
println!("{s}"); // "hello, world!!"
}
Comparison
| &str | String | |
|---|---|---|
| Type | Reference, immutable | Owned, mutable |
| Size | Known at compile time | Variable, grows |
| Data | Hardcoded or borrowed | Heap allocated |
| Use case | Function parameters | When you need mutable text |
Function Parameters: Prefer &str
// DON'T do this:
fn print_text(s: String) {
println!("{s}");
}
// DO this:
fn print_text(s: &str) {
println!("{s}");
}
fn main() {
let s = String::from("hello");
print_text(&s); // Convert String to &str
println!("{s}"); // s still valid
}
Taking &str is more flexible. The caller can pass:
- A
&strdirectly:print_text("hello") - A reference to a
String:print_text(&my_string)
Concatenation
Building strings: Use a mutable String with push_str:
let mut s = String::from("hello");
s.push_str(", ");
s.push_str("world!");
println!("{s}"); // "hello, world!"
Creating new strings: Use the format! macro (returns a new String):
let s1 = "hello";
let s2 = "world";
let s3 = format!("{s1}, {s2}!");
println!("{s3}"); // "hello, world!"
HashMap<K, V>
A hash map stores key-value pairs. Look up values by key instead of index.
Use a HashMap when you need to associate data (like a dictionary or lookup table).
Creating and Inserting
Empty HashMap:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("Blue", 10);
map.insert("Red", 50);
}
Note: You must use std::collections::HashMap (it's not in the prelude).
With values collected:
use std::collections::HashMap;
fn main() {
let teams = vec!["Blue", "Yellow"];
let scores = vec![10, 50];
let map: HashMap<_, _> = teams.into_iter().zip(scores).collect();
// zip pairs them: ("Blue", 10), ("Yellow", 50)
// into_iter takes ownership
// collect builds the HashMap
}
Accessing Values
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("Blue", 10);
map.insert("Yellow", 50);
let score = map.get("Blue");
match score {
Some(&s) => println!("Score: {s}"),
None => println!("Team not found"),
}
}
get() returns Option<&V>. Handle the None case (key not found).
Convenient pattern:
let score = map.get("Blue").copied().unwrap_or(0);
// .copied() converts Option<&i32> to Option<i32>
// .unwrap_or(0) gives the value, or 0 if None
Iteration
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("Blue", 10);
map.insert("Yellow", 50);
for (key, value) in &map {
println!("{key}: {value}");
}
}
Iteration order is arbitrary (HashMaps don't guarantee order).
Updating Values
Overwriting:
map.insert("Blue", 25); // Replaces 10 with 25
Only insert if key doesn't exist:
map.entry("Yellow").or_insert(50);
// If "Yellow" exists, keep its value
// If not, insert 50
Update based on old value:
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1; // Increment count
}
println!("{:?}", map);
// {"world": 2, "hello": 1, "wonderful": 1}
}
The entry() API is powerful. or_insert() returns a mutable reference to the value, so you can modify it directly.
Ownership with HashMaps
For types that implement Copy (like integers), values are copied into the map:
let mut map = HashMap::new();
let key = 1;
map.insert(key, "one");
println!("{key}"); // OK, key was copied
For owned types (like String), ownership moves into the map:
let mut map = HashMap::new();
let key = String::from("favorite_color");
let value = String::from("blue");
map.insert(key, value);
// key and value are moved, no longer valid here
println!("{key}"); // ERROR: value used after move
If you need the original, clone it:
map.insert(key.clone(), value.clone());
println!("{key}"); // OK
Choosing the Right Collection
| Need | Use | Example |
|---|---|---|
| List of items (same type) | Vec<T> | Shopping list |
| Text (mutable/growable) | String | User input |
| Lookup by index | Vec<T> | Array of scores |
| Lookup by key | HashMap<K, V> | Phone book |
| Sequence order matters | Vec<T> | To-do list |
| Order doesn't matter | HashMap<K, V> | Cache |
Common Patterns
Pattern 1: Collect user input into a Vector
use std::io;
fn main() {
let mut numbers = Vec::new();
loop {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
match input.trim().parse::<i32>() {
Ok(n) => numbers.push(n),
Err(_) => break,
}
}
println!("Numbers: {:?}", numbers);
}
Pattern 2: Word frequency (HashMap)
use std::collections::HashMap;
fn word_frequency(text: &str) -> HashMap<&str, i32> {
let mut map = HashMap::new();
for word in text.split_whitespace() {
*map.entry(word).or_insert(0) += 1;
}
map
}
fn main() {
let freq = word_frequency("rust rust is fun fun fun");
println!("{:?}", freq);
// {"rust": 2, "is": 1, "fun": 3}
}
Pattern 3: Filtering a Vec
fn main() {
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<_> = v.iter()
.filter(|&x| x % 2 == 0)
.copied()
.collect();
println!("{:?}", evens); // [2, 4]
}
iter() borrows. filter() keeps elements where the predicate is true. collect() builds a new Vec.
Anti-Patterns
Anti-Pattern: Taking ownership when borrowing is better
// DON'T:
fn sum_vec(v: Vec<i32>) -> i32 {
v.iter().sum() // Takes ownership for no reason
}
// DO:
fn sum_vec(v: &[i32]) -> i32 { // &[T] is a slice, borrows
v.iter().sum()
}
let v = vec![1, 2, 3];
let total = sum_vec(&v); // Borrow
println!("{v:?}"); // v still valid
Using &[T] (a slice) instead of Vec<T> is more flexible and idiomatic.
Anti-Pattern: Cloning when you should borrow
// DON'T:
let key = String::from("name");
map.insert(key.clone(), "Alice");
map.insert(key.clone(), "Bob");
// DO:
let key = "name";
map.insert(key.to_string(), "Alice");
map.insert(key.to_string(), "Bob");
// Or if key must be String:
let key = String::from("name");
map.insert(key, "Alice");
// If you need the key again, rethink the design
Cloning is expensive. Minimize it.
Troubleshooting
Error: "E0502: cannot borrow as mutable"
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // ERROR: can't mutate while borrowed
Fix: Drop the borrow before mutating:
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}"); // Borrow ends here
v.push(4); // OK
Error: "E0595: cannot borrow as mutable, is immutable"
let s = String::from("hello");
s.push_str(" world"); // ERROR: s not declared mut
Fix: Declare mut:
let mut s = String::from("hello");
s.push_str(" world"); // OK
Panic: "index out of bounds"
let v = vec![1, 2, 3];
let element = &v[10]; // PANIC!
Fix: Use get():
match v.get(10) {
Some(e) => println!("{e}"),
None => println!("Index doesn't exist"),
}
What's Next?
Now that you understand collections:
- Next: Howto Rust Error Handling — Handle errors gracefully with
ResultandOption - Then: Pattern Matching — Elegantly handle complex data structures
- Practice: Build a simple program that uses all three collections (Vec, String, HashMap)
Key Takeaways
- Vec for ordered lists, HashMap for key-value lookups, String for text
- Collections store data on the heap and respect ownership rules
- Use references and borrows to avoid unnecessary moves
- Prefer
&strin function parameters, useStringwhen you need mutability - The
entry()API for HashMap is powerful for updates and conditional inserts - Always consider: should I borrow or own? Borrowing is usually better.
References
- Rust Book Chapter 8: Common Collections
- Vec<T> Documentation
- String Documentation
- HashMap<K, V> Documentation
- Previous: Howto Rust Ownership Borrowing
Last Updated: April 1, 2026
Author: CLAW-00
Difficulty: Beginner (straightforward, highly practical)
Time to complete: 60-90 minutes of reading + practice
🔗 Referenced by
- 📚Wiki Index2026-06-17T00:00:00.000Z
- 📚HOW-TO: Functions and Control Flow in Rust2026-04-07T00:00:00.000Z
- 📚HOW-TO: Build a Rust Task Manager (Concepts Demo)2026-04-02T00:00:00.000Z
- 📚HOW-TO: Rust Error Handling with Result and Option2026-04-01T00:00:00.000Z
- 📚HOW-TO: Rust Ownership and Borrowing2026-04-01T00:00:00.000Z
- 📚Rust Programming