Rust Vec: Dynamic Arrays
The contiguous memory layout gives vectors the same cache-friendly access patterns as arrays, but with flexibility. When you need to store an unknown number of elements or modify collection size...
Key Insights
- Vec
is Rust’s growable array type that lives on the heap, offering dynamic sizing unlike fixed-size arrays while maintaining contiguous memory layout for cache efficiency - Pre-allocating capacity with
Vec::with_capacity()eliminates expensive reallocations when you know the approximate size, often improving performance by 2-3x for bulk operations - Use
get()instead of index access when bounds aren’t guaranteed—the Option return type forces you to handle missing elements safely without panicking
Introduction to Vec
Vec<T> is Rust’s dynamic array type, providing a growable, heap-allocated collection that stores elements of type T in contiguous memory. While arrays like [i32; 5] have fixed sizes known at compile time and live on the stack, vectors can grow and shrink at runtime, making them essential for real-world applications where collection sizes aren’t predetermined.
The contiguous memory layout gives vectors the same cache-friendly access patterns as arrays, but with flexibility. When you need to store an unknown number of elements or modify collection size during execution, Vec<T> is your default choice.
// Fixed-size array - stack allocated, size known at compile time
let array: [i32; 3] = [1, 2, 3];
// Vector - heap allocated, can grow dynamically
let mut vec = vec![1, 2, 3];
vec.push(4); // Can grow - arrays cannot
Creating and Initializing Vectors
Rust provides several ways to create vectors, each suited for different scenarios. Understanding the distinction between length (actual elements) and capacity (allocated space) is crucial for performance.
// Empty vector with zero capacity
let v1: Vec<i32> = Vec::new();
// Empty vector with pre-allocated capacity for 10 elements
let v2: Vec<i32> = Vec::with_capacity(10);
// Using the vec! macro - most common for initial values
let v3 = vec![1, 2, 3, 4, 5];
// Create vector with repeated values
let v4 = vec![0; 100]; // 100 zeros
// From arrays and slices
let arr = [1, 2, 3];
let v5 = Vec::from(arr);
let v6 = arr.to_vec();
// From iterators
let v7: Vec<i32> = (0..5).collect();
The with_capacity() constructor is particularly important when you know approximately how many elements you’ll need. It allocates memory upfront, avoiding multiple reallocations as the vector grows:
let mut v = Vec::with_capacity(1000);
println!("Length: {}, Capacity: {}", v.len(), v.capacity()); // Length: 0, Capacity: 1000
// Adding elements won't require reallocation until capacity is exceeded
for i in 0..1000 {
v.push(i); // No reallocations needed
}
Adding and Removing Elements
Vectors support several methods for modification, each with different performance characteristics. Operations at the end are O(1) amortized, while operations in the middle are O(n) due to element shifting.
let mut v = vec![1, 2, 3];
// Push to end - O(1) amortized
v.push(4);
println!("{:?}", v); // [1, 2, 3, 4]
// Pop from end - O(1), returns Option<T>
if let Some(last) = v.pop() {
println!("Removed: {}", last); // 4
}
// Insert at specific index - O(n), shifts elements right
v.insert(1, 10); // Insert 10 at index 1
println!("{:?}", v); // [1, 10, 2, 3]
// Remove at specific index - O(n), shifts elements left
let removed = v.remove(1);
println!("Removed: {}, Vec: {:?}", removed, v); // Removed: 10, Vec: [1, 2, 3]
// Truncate to specific length
v.truncate(2);
println!("{:?}", v); // [1, 2]
For bulk operations, use extend() or append():
let mut v1 = vec![1, 2, 3];
let mut v2 = vec![4, 5, 6];
// Extend from iterator
v1.extend(&[7, 8, 9]);
println!("{:?}", v1); // [1, 2, 3, 7, 8, 9]
// Append moves all elements from v2, leaving it empty
v1.append(&mut v2);
println!("{:?}", v1); // [1, 2, 3, 7, 8, 9, 4, 5, 6]
println!("{:?}", v2); // []
Accessing Elements
Rust provides both panic-inducing and safe access methods. Choose based on whether you can guarantee valid indices.
let v = vec![10, 20, 30, 40, 50];
// Direct indexing - panics if out of bounds
let third = v[2];
println!("Third element: {}", third); // 30
// Safe access with get() - returns Option<&T>
match v.get(2) {
Some(val) => println!("Third element: {}", val),
None => println!("Index out of bounds"),
}
// Convenient access to ends
if let Some(first) = v.first() {
println!("First: {}", first); // 10
}
if let Some(last) = v.last() {
println!("Last: {}", last); // 50
}
// Immutable iteration
for val in &v {
println!("{}", val);
}
// Mutable iteration
let mut v = vec![1, 2, 3];
for val in &mut v {
*val *= 2;
}
println!("{:?}", v); // [2, 4, 6]
// Iterator methods
let sum: i32 = v.iter().sum();
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
Always prefer get() when working with user input or untrusted indices. The compiler forces you to handle the None case, preventing runtime panics.
Memory Management and Performance
Understanding how vectors manage memory is critical for writing efficient code. Vectors grow by allocating new memory when capacity is exceeded, copying existing elements, and deallocating old memory.
let mut v = Vec::new();
println!("Initial - Len: {}, Cap: {}", v.len(), v.capacity());
for i in 0..10 {
v.push(i);
println!("After push {} - Len: {}, Cap: {}", i, v.len(), v.capacity());
}
// You'll see capacity grow: 0 -> 4 -> 8 -> 16 as needed
The growth strategy typically doubles capacity, providing O(1) amortized push performance. However, each reallocation is expensive:
use std::time::Instant;
// Without pre-allocation
let start = Instant::now();
let mut v1 = Vec::new();
for i in 0..100_000 {
v1.push(i);
}
let duration1 = start.elapsed();
// With pre-allocation
let start = Instant::now();
let mut v2 = Vec::with_capacity(100_000);
for i in 0..100_000 {
v2.push(i);
}
let duration2 = start.elapsed();
println!("Without capacity: {:?}", duration1);
println!("With capacity: {:?}", duration2);
// Pre-allocation is typically 2-3x faster
Use shrink_to_fit() to reduce memory footprint when you’re done growing a vector:
let mut v = Vec::with_capacity(100);
v.push(1);
v.push(2);
println!("Before shrink - Len: {}, Cap: {}", v.len(), v.capacity()); // 2, 100
v.shrink_to_fit();
println!("After shrink - Len: {}, Cap: {}", v.len(), v.capacity()); // 2, 2
Common Patterns and Methods
Vectors support rich operations for sorting, filtering, and searching:
let mut numbers = vec![5, 2, 8, 1, 9, 3];
// Sorting
numbers.sort();
println!("{:?}", numbers); // [1, 2, 3, 5, 8, 9]
// Custom sorting
numbers.sort_by(|a, b| b.cmp(a)); // Descending
println!("{:?}", numbers); // [9, 8, 5, 3, 2, 1]
// Filtering with retain
let mut v = vec![1, 2, 3, 4, 5, 6];
v.retain(|x| x % 2 == 0);
println!("{:?}", v); // [2, 4, 6]
// Binary search on sorted vectors - O(log n)
let numbers = vec![1, 3, 5, 7, 9, 11];
match numbers.binary_search(&7) {
Ok(index) => println!("Found at index {}", index),
Err(index) => println!("Not found, would insert at {}", index),
}
// Deduplication (requires sorted vector)
let mut v = vec![1, 2, 2, 3, 3, 3, 4];
v.dedup();
println!("{:?}", v); // [1, 2, 3, 4]
// Collecting from iterators
let evens: Vec<i32> = (0..10)
.filter(|x| x % 2 == 0)
.collect();
println!("{:?}", evens); // [0, 2, 4, 6, 8]
Best Practices and Pitfalls
Choose the right collection for your use case. Use arrays when size is fixed and known at compile time. Use slices (&[T]) for function parameters to accept both arrays and vectors. Use Vec<T> when you need dynamic sizing.
// Good: Function accepts any contiguous sequence
fn process_numbers(nums: &[i32]) -> i32 {
nums.iter().sum()
}
let arr = [1, 2, 3];
let vec = vec![4, 5, 6];
println!("{}", process_numbers(&arr)); // Works
println!("{}", process_numbers(&vec)); // Works too
Avoid common mistakes:
// DON'T: Unnecessary cloning
let v1 = vec![1, 2, 3];
let v2 = v1.clone(); // Expensive if you don't need both
// DO: Move ownership if you don't need original
let v1 = vec![1, 2, 3];
let v2 = v1; // v1 no longer accessible, but no allocation
// DON'T: Growing without capacity hint
let mut v = Vec::new();
for i in 0..10000 {
v.push(i); // Multiple reallocations
}
// DO: Pre-allocate when size is known
let mut v = Vec::with_capacity(10000);
for i in 0..10000 {
v.push(i); // No reallocations
}
// DON'T: Index access with untrusted input
fn get_element(v: &Vec<i32>, index: usize) -> i32 {
v[index] // Panics if index >= v.len()
}
// DO: Use get() for safe access
fn get_element_safe(v: &Vec<i32>, index: usize) -> Option<i32> {
v.get(index).copied()
}
Remember that vectors own their data. When a vector goes out of scope, all elements are dropped. This makes vectors unsuitable for scenarios requiring shared ownership—consider Rc<Vec<T>> or Arc<Vec<T>> for those cases.
Vec<T> is the workhorse collection in Rust. Master its API, understand its performance characteristics, and you’ll write more efficient, idiomatic Rust code.