Vectors
Beginner · Abstractions
What & why
Arrays in Rust have a size fixed at compile time — [i32; 3] is always exactly 3 integers, forever. Almost nothing in real programs works that way: shopping carts grow, search results come back with an unknown count, logs accumulate one line at a time. Vec<T> is Rust’s growable list — a row of same-typed boxes you can keep adding to (or removing from) while the program runs. It’s the collection you reach for by default.
The idea, slowly
Building one: Vec::new() vs vec![...]
fn main() {
let mut a: Vec<i32> = Vec::new(); // empty, type must be known somehow
a.push(1);
a.push(2);
let b = vec![10, 20, 30]; // vec! macro — pre-filled, type inferred
println!("{:?}", a);
println!("{:?}", b);
}
Vec::new() starts empty and figures out its element type either from an annotation (Vec<i32>) or from the first thing you push into it. vec![...] is a macro (note the !) that builds a Vec already holding the values you list — reach for it whenever you know the starting contents up front. {:?} is the “debug” format specifier; it can print a whole collection at once, which plain {} cannot.
push and pop
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
println!("{:?}", stack); // [1, 2, 3]
// pop returns Option<T> — Some(value) if there was one, None if empty
match stack.pop() {
Some(top) => println!("popped {}", top), // popped 3
None => println!("nothing to pop"),
}
println!("{:?}", stack); // [1, 2]
}
push adds to the end; pop removes and returns the last element. Because an empty Vec has nothing to pop, pop() hands back an Option<T> instead of just T — Some(value) if something was there, None if the vector was empty. This is the same “don’t crash, tell the caller” pattern you’ve already met with Option.
Indexing: v[i] panics, .get(i) doesn’t
fn main() {
let nums = vec![10, 20, 30];
println!("{}", nums[1]); // 20 — fine, index 1 exists
match nums.get(10) {
Some(n) => println!("found {}", n),
None => println!("nothing at index 10"), // this runs
}
// nums[10]; // would PANIC: index out of bounds
}
v[i] reads by position, counting from 0, and panics (crashes the program on purpose) if i is out of range. .get(i) is the crash-free alternative — it returns Option<&T>, Some(&value) if the index exists and None if it doesn’t. Use v[i] when you already know the index is valid (e.g. you just checked v.len()); use .get(i) whenever the index comes from somewhere you don’t fully trust, like user input.
Iterating
fn main() {
let nums = vec![1, 2, 3];
for n in &nums {
// n: &i32 — borrowing, nums still usable afterward
print!("{} ", n);
}
println!();
let mut mutable_nums = vec![1, 2, 3];
for n in &mut mutable_nums {
*n *= 10; // n: &mut i32 — modify in place
}
println!("{:?}", mutable_nums); // [10, 20, 30]
for n in nums {
// n: i32 — this CONSUMES nums; it can't be used after this loop
print!("{} ", n);
}
println!();
}
for x in &v borrows and yields &T — the vector is untouched and usable afterward. for x in &mut v borrows mutably and yields &mut T — you can modify elements through it. for x in v (no &) takes ownership of the vector and hands you owned T values one at a time; after that loop, v is gone. Reach for &v by far the most often.
Sorting: sort and sort_by
fn main() {
let mut nums = vec![5, 1, 4, 2, 3];
nums.sort(); // ascending, uses the type's natural ordering
println!("{:?}", nums); // [1, 2, 3, 4, 5]
let mut words = vec!["pear", "fig", "kiwi"];
words.sort_by(|a, b| a.len().cmp(&b.len())); // custom comparator: shortest first
println!("{:?}", words); // ["fig", "kiwi", "pear"]
let mut people = vec![("Bea", 41), ("Al", 30), ("Cy", 25)];
people.sort_by_key(|p| p.1); // sort by age — often clearer than sort_by
println!("{:?}", people); // [("Cy", 25), ("Al", 30), ("Bea", 41)]
}
sort() works for types with a natural order (numbers, strings, …). sort_by takes a closure that compares two elements and returns an Ordering — use it for custom rules. sort_by_key is the common special case “sort by this one field,” and reads more clearly than a full comparator when that’s all you need.
retain: keep only what passes a test
fn main() {
let mut nums = vec![1, 2, 3, 4, 5, 6];
nums.retain(|&n| n % 2 == 0); // keep only even numbers
println!("{:?}", nums); // [2, 4, 6]
}
retain walks the vector and removes every element for which the closure returns false, in place — no separate “filter into a new vector” step needed when you just want to prune what’s already there.
dedup: only removes consecutive duplicates
fn main() {
let mut nums = vec![1, 1, 2, 2, 2, 1, 3];
nums.dedup();
println!("{:?}", nums); // [1, 2, 1, 3] — NOT fully deduplicated!
let mut nums2 = vec![1, 1, 2, 2, 2, 1, 3];
nums2.sort(); // [1, 1, 1, 2, 2, 2, 3]
nums2.dedup(); // now every duplicate is adjacent
println!("{:?}", nums2); // [1, 2, 3]
}
dedup only collapses runs of adjacent equal elements — it does not scan the whole vector for duplicates anywhere. If you want every duplicate gone regardless of position, sort() first so equal elements become neighbors, then dedup().
Vec::with_capacity: avoid reallocating while you grow
fn main() {
let mut a = Vec::new(); // capacity 0 — first few pushes reallocate
for i in 0..5 {
a.push(i);
}
let mut b: Vec<i32> = Vec::with_capacity(5); // room for 5 reserved up front
for i in 0..5 {
b.push(i); // none of these pushes need to reallocate
}
println!("{:?} {:?}", a, b);
}
A Vec is backed by one contiguous block of heap memory. When it runs out of room, push has to allocate a bigger block, copy every existing element over, and free the old block — an O(n) operation that happens occasionally as a vector grows (Rust’s standard library roughly doubles capacity each time, so this happens less and less often, but it still happens). If you know up front roughly how many elements you’ll end up with, Vec::with_capacity(n) reserves that space once, so the pushes that follow don’t trigger any reallocation at all. v.len() is how many elements are actually there; v.capacity() is how much room is reserved — they’re allowed to differ.
Common mistakes
- Forgetting
mut.push,pop,sort,retain, anddedupall mutate the vector, so it must belet mut. The error iscannot borrow as mutable. - Indexing out of bounds.
v[99]on a short vector panics at runtime withindex out of bounds. When the index isn’t already known to be valid, usev.get(99)and handle theOptioninstead. - Treating
pop()as if it returns the value directly. It returnsOption<T>, because there might be nothing left to pop.let x: i32 = v.pop();is a type error — you needv.pop().unwrap()(if you’re sure), or amatch/if let. - Expecting
dedup()to remove all duplicates. It only merges adjacent equal runs. Duplicates scattered through an unsorted vector survivededup()untouched — sort first. - Skipping
Vec::with_capacityin a hot loop. Repeatedly pushing into aVec::new()when you already know the final size causes avoidable reallocations and copies. Not wrong, just slower than it needs to be. - Trying to print a
Vecwith{}. Use{:?}(debug format) for whole vectors; plain{}only works for single values with aDisplayimplementation.
More examples
Shopping cart total
Once prices are in a Vec, .iter().sum() turns “add up every item” into one line instead of a hand-rolled loop.
fn main() {
let cart = vec![19.99, 5.50, 3.25, 12.00];
let total: f64 = cart.iter().sum();
println!("cart total: ${:.2}", total);
}
Deduplicating scraped URLs
A scraper that follows links will see the same page more than once. Sorting then dedup-ing turns a messy list of visited pages into the distinct set.
fn main() {
let mut urls = vec![
"site.com/a".to_string(),
"site.com/b".to_string(),
"site.com/a".to_string(),
"site.com/c".to_string(),
"site.com/b".to_string(),
];
urls.sort();
urls.dedup();
println!("{} unique pages found", urls.len());
println!("{:?}", urls);
}
An undo history as a stack
A text editor’s undo button always undoes the most recent action — exactly what push/pop on a Vec give you for free.
fn main() {
let mut history: Vec<String> = Vec::new();
history.push("typed 'hello'".to_string());
history.push("bolded text".to_string());
history.push("inserted image".to_string());
if let Some(last_action) = history.pop() {
println!("undoing: {}", last_action);
}
println!("remaining history: {:?}", history);
}
Chunking a list into batches
An email service that only accepts 2 recipients per request needs the full list broken into fixed-size groups first — .chunks(n) does exactly that without any manual index math.
fn main() {
let emails = vec!["a@x.com", "b@x.com", "c@x.com", "d@x.com", "e@x.com"];
for (i, batch) in emails.chunks(2).enumerate() {
println!("sending batch {}: {:?}", i + 1, batch);
}
}
Removing an item by value
Banning a user should remove every occurrence of their name from a list, not just one — retain keeps everything that doesn’t match.
fn main() {
let mut usernames = vec!["alice", "spam_bot", "bob", "spam_bot", "carol"];
usernames.retain(|&name| name != "spam_bot");
println!("{:?}", usernames);
}
Your turn
This program should pop the top of a stack and print it, but it doesn’t compile.
fn main() {
let mut stack = vec![1, 2, 3];
let top: i32 = stack.pop();
println!("top: {}", top);
}
Show solution
stack.pop() returns Option<i32>, not i32 directly — the vector might have been empty, so pop has to be able to say “nothing here.” The annotation let top: i32 = ... demands an i32, and the compiler catches the mismatch (expected i32, found Option<i32>) before the program ever runs.
fn main() {
let mut stack = vec![1, 2, 3];
match stack.pop() {
Some(top) => println!("top: {}", top),
None => println!("stack was empty"),
}
}
Handling both cases with match (or .unwrap() if you’re certain the vector is non-empty) is what pop’s Option return type is asking you to do.
Quick check
Remember this
Vec::new()starts empty;vec![...]builds one pre-filled. Both needlet mutto be modified.v[i]panics on an out-of-range index;v.get(i)returnsOption<&T>instead — no crash.pop()returnsOption<T>, notT, because the vector might be empty.for x in &vborrows (vector stays usable);for x in vconsumes it.sort/sort_by/sort_by_keyreorder in place;retainkeeps only elements passing a test;dedupremoves only adjacent duplicates, so sort first if you want them all gone.Vec::with_capacity(n)reserves space up front and avoids the reallocate-and-copy cost of growing one push at a time.
Go deeper
- std::vec::Vec docs — Full Vec API.
Next: