Rust Vectors
last modified February 19, 2025
In this article we show how to work with vectors in Rust. A Vec<T>
is a contiguous, growable array type that is the most commonly used collection in Rust.
Vectors are stored on the heap and automatically resize themselves when new elements are added. They provide fast random access, efficient iteration, and seamless integration with Rust's ownership and borrowing system.
Rust Vector Creation
We can create vectors using the vec! macro, the Vec::new()
constructor, or the with_capacity() method for pre-allocation.
fn main() {
// Using the vec! macro
let numbers = vec![1, 2, 3, 4, 5];
// Using Vec::new() and pushing elements
let mut words = Vec::new();
words.push(String::from("Hello"));
words.push(String::from("Rust"));
// Pre-allocating capacity for performance
let mut data: Vec<i32> = Vec::with_capacity(100);
for i in 0..10 {
data.push(i * 2);
}
println!("Numbers: {:?}", numbers);
println!("Words: {:?}", words);
println!("Data (capacity: {}): {:?}", data.capacity(), data);
}
The vec! macro is the most concise way to initialize a vector with known values.
with_capacity() avoids reallocations when you know the approximate size upfront.
let numbers = vec![1, 2, 3, 4, 5]; let mut data: Vec<i32> = Vec::with_capacity(100);
Type inference usually works with vec!, but explicit type annotation
is sometimes needed for empty or complex vectors.
λ cargo run -q Numbers: [1, 2, 3, 4, 5] Words: ["Hello", "Rust"] Data (capacity: 100): [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Accessing Vector Elements
We can access elements using indexing [] or the safer get() method.
fn main() {
let fruits = vec!["Apple", "Banana", "Cherry", "Date"];
// Direct indexing (panics if out of bounds)
let first = fruits[0];
let last = fruits[fruits.len() - 1];
println!("First: {}, Last: {}", first, last);
// Safe access with get() returns Option
match fruits.get(2) {
Some(fruit) => println!("Index 2: {}", fruit),
None => println!("Index out of bounds"),
}
if let Some(none) = fruits.get(10) {
println!("Found: {}", none);
} else {
println!("Index 10 does not exist");
}
}
Indexing is fast but unsafe for dynamic data. Use get() when working
with user input or uncertain bounds.
match fruits.get(2) {
Some(fruit) => println!("Index 2: {}", fruit),
None => println!("Index out of bounds"),
}
The get() method returns Option<&T>, forcing
explicit handling of missing elements at compile time.
λ cargo run -q First: Apple, Last: Date Index 2: Cherry Index 10 does not exist
Adding Elements
We can add elements to the end with push(), at a specific index
with insert(), or merge another collection with extend().
fn main() {
let mut scores = vec![10, 20];
// Add to the end
scores.push(30);
// Insert at index 1 (shifts existing elements)
scores.insert(1, 15);
// Append multiple values
scores.extend([40, 50]);
println!("{:?}", scores);
}
insert() has O(n) complexity because it shifts all
subsequent elements. Use it sparingly for large vectors.
scores.insert(1, 15); // [10, 15, 20, 30] scores.extend([40, 50]); // [10, 15, 20, 30, 40, 50]
λ cargo run -q [10, 15, 20, 30, 40, 50]
Removing Elements
Vectors provide several removal methods: pop() for the last element,
remove() for arbitrary indices, and swap_remove() for
O(1) removal when order doesn't matter.
fn main() {
let mut items = vec!["A", "B", "C", "D", "E"];
// Remove and return last element
let last = items.pop();
println!("Popped: {:?}", last);
// Remove at index 1 (shifts elements left)
items.remove(1);
println!("After remove: {:?}", items);
// Fast O(1) removal (swaps with last, then pops)
items.swap_remove(0);
println!("After swap_remove: {:?}", items);
// Clear all elements (keeps capacity)
items.clear();
println!("Cleared: {:?}", items);
}
swap_remove() is highly efficient for performance-critical code
where element order isn't important, such as in game object pools or particle systems.
λ cargo run -q
Popped: Some("E")
After remove: ["A", "C", "D"]
After swap_remove: ["D", "C"]
Cleared: []
Iterating Over Vectors
We can iterate over vectors immutably, mutably, or consume the vector entirely.
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
// Immutable iteration (borrows elements)
println!("Immutable:");
for n in &numbers {
print!("{} ", n);
}
println!();
// Mutable iteration (borrows mutably)
for n in &mut numbers {
*n *= 2;
}
println!("Mutable: {:?}", numbers);
// Consuming iteration (takes ownership)
let strings = vec![String::from("Hello"), String::from("World")];
for s in strings {
println!("Consumed: {}", s);
}
// strings is no longer valid here
}
Choose the iteration style based on whether you need to read, modify, or transfer ownership.
for n in &mut numbers {
*n *= 2; // Dereference mutable borrow to modify
}
λ cargo run -q Immutable: 1 2 3 4 5 Mutable: [2, 4, 6, 8, 10] Consumed: Hello Consumed: World
Capacity vs Length
A vector's length is the number of elements currently stored. Its capacity is the allocated memory space. Vectors grow by allocating larger buffers when capacity is exceeded.
fn main() {
let mut v = Vec::with_capacity(3);
println!("Initial - len: {}, cap: {}", v.len(), v.capacity());
v.push(1);
v.push(2);
v.push(3);
println!("Full - len: {}, cap: {}", v.len(), v.capacity());
// Exceeds capacity, triggers reallocation
v.push(4);
println!("Grown - len: {}, cap: {}", v.len(), v.capacity());
// Reduce memory footprint
v.shrink_to_fit();
println!("Shrunk - len: {}, cap: {}", v.len(), v.capacity());
}
Understanding capacity helps optimize memory usage. Vectors typically double
their capacity on reallocation to achieve amortized O(1) push time.
λ cargo run -q Initial - len: 0, cap: 3 Full - len: 3, cap: 3 Grown - len: 4, cap: 6 Shrunk - len: 4, cap: 4
Slicing Vectors
We can create slices to view a portion of a vector without copying data. Slices are dynamically-sized views into contiguous memory.
fn main() {
let data = vec![10, 20, 30, 40, 50, 60];
// Create slices
let first_half = &data[..3];
let middle = &data[1..4];
let last = &data[4..];
println!("First half: {:?}", first_half);
println!("Middle: {:?}", middle);
println!("Last: {:?}", last);
// Slices can be passed to functions efficiently
print_sum(&data[2..5]);
}
fn print_sum(slice: &[i32]) {
let sum: i32 = slice.iter().sum();
println!("Slice sum: {}", sum);
}
Slices are lightweight references. Prefer &[T] in function signatures
over &Vec<T> for flexibility.
λ cargo run -q First half: [10, 20, 30] Middle: [20, 30, 40] Last: [50, 60] Slice sum: 90
Sorting and Searching
Vectors can be sorted in-place and searched efficiently using binary search after sorting.
fn main() {
let mut nums = vec![42, 7, 19, 3, 99, 15];
// Sort in ascending order
nums.sort();
println!("Sorted: {:?}", nums);
// Custom sort (descending)
nums.sort_by(|a, b| b.cmp(a));
println!("Descending: {:?}", nums);
// Binary search (requires sorted data)
match nums.binary_search(&19) {
Ok(idx) => println!("Found 19 at index {}", idx),
Err(_) => println!("19 not found"),
}
// Contains check
println!("Contains 42? {}", nums.contains(&42));
}
sort() uses a stable, adaptive algorithm. For primitive types where
stability doesn't matter, sort_unstable() is often faster.
λ cargo run -q Sorted: [3, 7, 15, 19, 42, 99] Descending: [99, 42, 19, 15, 7, 3] Found 19 at index 2 Contains 42? true
Transforming Vectors
Using iterator methods like map(), filter(), and
collect(), we can transform vectors functionally.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filter even numbers, then square them
let processed: Vec<i32> = numbers
.iter()
.filter(|&x| x % 2 == 0)
.map(|x| x * x)
.collect();
println!("Processed: {:?}", processed);
// Convert to different type
let strings: Vec<String> = numbers
.into_iter()
.map(|n| format!("Number {}", n))
.collect();
println!("Strings: {:?}", strings);
}
Iterator chains are zero-cost abstractions in Rust. The compiler optimizes them into tight loops with no intermediate allocations.
λ cargo run -q Processed: [4, 16, 36, 64, 100] Strings: ["Number 1", "Number 2", "Number 3", "Number 4", "Number 5", "Number 6", "Number 7", "Number 8", "Number 9", "Number 10"]
Best Practices
Follow these guidelines for efficient and idiomatic vector usage:
- Pre-allocate when possible: Use
with_capacity()orreserve()if you know the size. - Prefer slices in APIs: Accept
&[T]instead of&Vec<T>for greater flexibility. - Use
swap_remove(): When order doesn't matter, it avoidsO(n)shifts. - Avoid
clone()in loops: Work with references or usedrain()to transfer ownership. - Leverage iterators: They're often faster and more expressive than manual indexing.
// Good: Accept slice reference
fn process_data(data: &[f64]) -> f64 {
data.iter().sum::() / data.len() as f64
}
// Good: Pre-allocate for known size
fn generate_squares(n: usize) -> Vec<u32> {
let mut result = Vec::with_capacity(n);
for i in 0..n {
result.push((i as u32) * (i as u32));
}
result
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
println!("Average: {:.2}", process_data(&data));
println!("Squares: {:?}", generate_squares(5));
}
λ cargo run -q Average: 3.00 Squares: [0, 1, 4, 9, 16]
Conclusion
Vectors are the backbone of collection handling in Rust. Their combination of heap allocation, dynamic resizing, and tight integration with the borrow checker makes them both safe and performant.
Key takeaways:
- Use
vec!for initialization andpush()for growth. - Prefer
get()for safe bounds-checked access. - Understand capacity vs length to optimize memory usage.
- Use slices (
&[T]) in function signatures for flexibility. - Leverage iterators for clean, zero-cost transformations.
Mastering vectors will significantly improve your ability to write efficient, idiomatic Rust code.
Author
List all Rust tutorials.