Vectors in Rust
A Vec
Creating a Vector
There are three common ways to create a vector.
// 1. Empty vector — type must be known via annotation or a later push
let mut v: Vec<i32> = Vec::new();
// 2. vec! macro — most concise, infers the type from the values
let v = vec![1, 2, 3, 4, 5];
// 3. Pre-allocated capacity — avoids reallocations when size is known
let mut v: Vec<String> = Vec::with_capacity(100);
println!("len={}, cap={}", v.len(), v.capacity()); // len=0, cap=100Adding Elements
let mut v = vec![1, 2, 3]; // Push a single element to the end — O(1) amortised v.push(4); // [1, 2, 3, 4] // Extend with any iterable v.extend([5, 6, 7]); // [1, 2, 3, 4, 5, 6, 7] // append drains another Vec into this one (the other becomes empty) let mut other = vec![8, 9]; v.append(&mut other); // v = [1..9], other = []
Reading Elements
Rust gives you two ways to read an element. The choice determines how the program handles an out-of-bounds index.
let v = vec![10, 20, 30];
// Index operator — panics at runtime if the index is out of bounds
let x = v[1];
println!("{}", x); // 20
// .get() — returns Option<&T>, safe for untrusted indices
match v.get(10) {
Some(val) => println!("Got {}", val),
None => println!("Index out of range"),
}Iterating
There are three iteration patterns. The one you choose determines ownership of the vector.
let mut v = vec![1, 2, 3];
// Immutable borrow — v is still usable after the loop
for x in &v {
println!("{}", x);
}
// Mutable borrow — modify elements in place
for x in &mut v {
*x *= 2;
}
println!("{:?}", v); // [2, 4, 6]
// Consuming iteration — v is moved into the loop and dropped afterwards
for x in v {
println!("{}", x);
}
// v cannot be used hereModifying Elements
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
// Remove and return the last element
if let Some(last) = v.pop() {
println!("Popped: {}", last); // 6
}
// Remove element at index (shifts elements left) — O(n)
let removed = v.remove(2); // removes the element at index 2
// Insert at index (shifts elements right) — O(n)
v.insert(0, 99);
// Keep only elements satisfying a predicate
v.retain(|&x| x > 2);
// Remove consecutive duplicates (sort first if you want ALL duplicates removed)
let mut d = vec![1, 1, 2, 3, 3, 3, 4];
d.dedup();
println!("{:?}", d); // [1, 2, 3, 4]Sorting
let mut nums = vec![5, 2, 8, 1, 9];
// Ascending sort — requires Ord
nums.sort();
println!("{:?}", nums); // [1, 2, 5, 8, 9]
// Custom comparator — descending
nums.sort_by(|a, b| b.cmp(a));
println!("{:?}", nums); // [9, 8, 5, 2, 1]
// Sort structs by a key field
#[derive(Debug)]
struct Person { name: String, age: u32 }
let mut people = vec![
Person { name: "Alice".into(), age: 30 },
Person { name: "Bob".into(), age: 25 },
];
people.sort_by_key(|p| p.age);
println!("{:?}", people); // Bob (25) before Alice (30)
// f64 does not implement Ord — use partial_cmp
let mut floats = vec![3.1_f64, 1.5, 2.7];
floats.sort_by(|a, b| a.partial_cmp(b).unwrap());Searching
let v = vec![10, 20, 30, 40, 50];
// Check membership — O(n) linear scan
println!("{}", v.contains(&30)); // true
// Find the first matching index — O(n)
let pos = v.iter().position(|&x| x == 30);
println!("{:?}", pos); // Some(2)
// Binary search on a sorted vector — O(log n)
match v.binary_search(&30) {
Ok(idx) => println!("Found at index {}", idx), // 2
Err(idx) => println!("Would insert at {}", idx),
}Transforming with Iterators
Vectors integrate seamlessly with Rust's iterator adapter chain. The chain is lazy — no work happens until you call a consuming adapter like .collect().
let v = vec![1, 2, 3, 4, 5, 6];
// Map every element into a new Vec
let doubled: Vec<i32> = v.iter().map(|&x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10, 12]
// Filter elements
let evens: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6]
// Chain adapters — filter then map
let result: Vec<i32> = v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 10)
.collect();
println!("{:?}", result); // [20, 40, 60]
// Enumerate — pairs each element with its index
for (i, val) in v.iter().enumerate() {
println!("v[{}] = {}", i, val);
}
// Aggregate operations
let sum: i32 = v.iter().sum();
let max = v.iter().max();
let product: i32 = v.iter().product();
println!("sum={}, max={:?}, product={}", sum, max, product);Slicing
A slice &[T] is a view into a contiguous sequence. Vectors coerce to slices automatically, so any function accepting &[T] also accepts a &Vec<T>.
let v = vec![0, 1, 2, 3, 4, 5];
// Borrow a range — exclusive end
let mid: &[i32] = &v[1..3];
println!("{:?}", mid); // [1, 2]
// From index 2 to the end
let tail = &v[2..];
println!("{:?}", tail); // [2, 3, 4, 5]
// Functions that accept &[T] work with both Vec and arrays
fn sum_slice(s: &[i32]) -> i32 { s.iter().sum() }
println!("{}", sum_slice(&v)); // 15Draining a Range
.drain() removes a range from a vector and returns an iterator over the removed elements. It is more efficient than repeated .remove() calls because it only shifts the remaining elements once.
let mut v = vec![1, 2, 3, 4, 5];
// Remove elements at indices 1 and 2, collect them
let drained: Vec<i32> = v.drain(1..3).collect();
println!("drained: {:?}", drained); // [2, 3]
println!("remaining: {:?}", v); // [1, 4, 5]
// Drain everything — empties the Vec but keeps its allocation
let mut v2 = vec![10, 20, 30];
for item in v2.drain(..) {
println!("processing {}", item);
}
// v2 is now empty but its buffer is still allocatedSplitting and Chunking
let v = vec![1, 2, 3, 4, 5, 6];
// split_at — returns two non-overlapping slices
let (left, right) = v.split_at(3);
println!("{:?} | {:?}", left, right); // [1, 2, 3] | [4, 5, 6]
// chunks — non-overlapping windows of fixed size
for chunk in v.chunks(2) {
println!("{:?}", chunk); // [1,2] [3,4] [5,6]
}
// windows — overlapping views of fixed size
for window in v.windows(3) {
println!("{:?}", window); // [1,2,3] [2,3,4] [3,4,5] [4,5,6]
}Storing Different Types with Enums
Vec
#[derive(Debug)]
enum Cell {
Int(i64),
Float(f64),
Text(String),
}
let row: Vec<Cell> = vec![
Cell::Int(42),
Cell::Float(3.14),
Cell::Text("hello".into()),
];
for cell in &row {
match cell {
Cell::Int(n) => println!("int: {}", n),
Cell::Float(f) => println!("float: {}", f),
Cell::Text(s) => println!("text: {}", s),
}
}Heterogeneous Collections with Trait Objects
When the set of types is open-ended (not known at compile time), use Vec<Box<dyn Trait>>.
trait Draw {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Draw for Circle { fn draw(&self) { println!("Circle r={}", self.radius); } }
impl Draw for Rectangle { fn draw(&self) { println!("Rect {}x{}", self.width, self.height); } }
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
];
for shape in &shapes {
shape.draw();
}Capacity vs Length
A Vec tracks two sizes: length (elements currently stored) and capacity (elements it can hold before the next reallocation). When length reaches capacity, the Vec allocates a larger buffer — typically doubling — and moves everything over.
let mut v: Vec<i32> = Vec::new();
println!("len={} cap={}", v.len(), v.capacity()); // len=0 cap=0
v.push(1);
println!("len={} cap={}", v.len(), v.capacity()); // len=1 cap=4 (exact cap varies)
// Release excess capacity back to the allocator
v.shrink_to_fit();
// Reserve additional room without changing length
v.reserve(50);
println!("cap >= 51: {}", v.capacity() >= 51); // true
// Truncate — keep only the first n elements
let mut v = vec![1, 2, 3, 4, 5];
v.truncate(3);
println!("{:?}", v); // [1, 2, 3]
// Clear all elements, keeping the allocation for future pushes
v.clear();
println!("len={}", v.len()); // 0Quick Reference
Operation | Method | Notes |
|---|---|---|
Create empty | Vec::new() | Allocates nothing yet |
Create with values | vec![1, 2, 3] | Most ergonomic |
Pre-allocate | Vec::with_capacity(n) | Avoids reallocations |
Add to end | .push(val) | O(1) amortised |
Remove from end | .pop() | Returns Option<T> |
Access safely | .get(i) | Returns Option<&T> |
Access unsafely | v[i] | Panics on out-of-bounds |
Remove at index | .remove(i) | O(n) — shifts elements |
Insert at index | .insert(i, val) | O(n) — shifts elements |
Keep matching | .retain(|x| pred) | In-place filter |
Sort | .sort() / .sort_by() | Stable in-place sort |
Find index | .iter().position() | O(n) linear search |
Binary search | .binary_search(&val) | O(log n), vector must be sorted |
Drain range | .drain(1..3) | Removes and yields elements |
Slice view | &v[1..3] | Coerces to &[T] |
Chunk iteration | .chunks(n) | Non-overlapping windows |