ZetCode

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.

main.rs
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.

main.rs
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().

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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:

main.rs
// 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:

Mastering vectors will significantly improve your ability to write efficient, idiomatic Rust code.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.