The Rust Collections Guide

Vec<T>: The Default Collection

Last Updated: 2026-04-05

Why `Vec<T>` is the default collection

Vec<T> is the general-purpose growable sequence type in Rust. If you need an ordered collection of values and do not yet have a strong reason to choose something else, Vec<T> is usually the right place to start.

It stores elements contiguously, supports fast iteration, works naturally with slices, and has a rich API for building, transforming, sorting, and searching data. Because the elements are contiguous in memory, vectors are also often cache-friendly in practice.

A useful mental model is that Vec<T> gives you three things at once: ownership of a sequence, dynamic length, and contiguous layout. That combination covers a large share of real programming tasks.

fn main() {
    let numbers = vec![10, 20, 30];
    println!("numbers = {:?}", numbers);
}

This is simple, but it already shows the main purpose of a vector: owning a sequence whose size can be chosen or changed at runtime.

The shape of a vector: pointer, length, and capacity

A vector conceptually tracks three pieces of information: where its element buffer lives, how many elements are currently present, and how much total space has been reserved. The current number of stored elements is the length. The total space available before another reallocation may be needed is the capacity.

This distinction matters because vectors often grow gradually. Appending a new item may fit within existing capacity, or it may require allocating a larger buffer and moving elements.

fn main() {
    let mut values = Vec::new();
    println!("len = {}, cap = {}", values.len(), values.capacity());
 
    values.push(1);
    values.push(2);
    values.push(3);
 
    println!("values = {:?}", values);
    println!("len = {}, cap = {}", values.len(), values.capacity());
}

The exact growth pattern is an implementation detail you should not rely on, but the idea of length versus capacity is fundamental. Length tells you how many valid elements exist. Capacity tells you how much room is available for future growth.

Creating vectors

Rust provides several common ways to create vectors. You can use the vec! macro for literal contents, Vec::new() for an empty vector, or collect values from an iterator.

fn main() {
    let a = vec![1, 2, 3];
    let b: Vec<i32> = Vec::new();
    let c: Vec<i32> = (1..=5).collect();
 
    println!("a = {:?}", a);
    println!("b = {:?}", b);
    println!("c = {:?}", c);
}

The vec! macro is most convenient when you already know the initial elements. Vec::new() is useful when building incrementally. Collecting from iterators is especially common when transforming existing data into a new vector.

Growing vectors with `push` and `extend`

The most basic way to grow a vector is push, which appends one element to the end. For adding multiple items, extend can append elements from another iterator.

fn main() {
    let mut values = Vec::new();
    values.push(10);
    values.push(20);
    values.push(30);
 
    values.extend([40, 50, 60]);
 
    println!("values = {:?}", values);
}

Appending at the end is one of the most natural and efficient vector operations. This is one reason Vec<T> fits so many workloads well. If your main pattern is accumulating items and later iterating over them, a vector is often exactly the right tool.

Preallocating with `with_capacity` and `reserve`

When you roughly know how many elements you will store, preallocating can reduce reallocations. Vec::with_capacity creates an empty vector with space reserved up front. reserve adds more spare capacity later.

fn main() {
    let mut values = Vec::with_capacity(100);
    println!("len = {}, cap = {}", values.len(), values.capacity());
 
    for n in 0..50 {
        values.push(n);
    }
 
    println!("len = {}, cap = {}", values.len(), values.capacity());
 
    values.reserve(100);
    println!("after reserve: len = {}, cap = {}", values.len(), values.capacity());
}

This does not change the vector's length. It only prepares room for future elements. Preallocation is most useful when repeated growth would otherwise trigger avoidable reallocations in performance-sensitive code.

Indexing and safe element access

Vectors support indexing by position because their elements are contiguous. Indexing with [] is convenient, but it will panic if the index is out of bounds. The get method is safer when an index might be invalid because it returns an Option.

fn main() {
    let values = vec![100, 200, 300];
 
    println!("first = {}", values[0]);
    println!("second via get = {:?}", values.get(1));
    println!("missing via get = {:?}", values.get(10));
}

As a rule, use [] when you know an index is valid as part of the program logic. Use get when invalid input or dynamic computation could produce an out-of-bounds index.

Borrowing vectors as slices

A vector owns its data, but many functions should not require ownership. One of the great strengths of Vec<T> is that it can easily be borrowed as a slice.

fn print_sum(values: &[i32]) {
    let sum: i32 = values.iter().sum();
    println!("sum = {sum}");
}
 
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    print_sum(&numbers);
    print_sum(&numbers[1..4]);
}

This is a central Rust pattern. Store dynamically sized owned data in a vector, but expose borrowed slice views at API boundaries whenever ownership is unnecessary.

Iterating over vectors

Vectors support several common iteration styles. You can iterate by shared reference, mutable reference, or by value.

fn main() {
    let mut values = vec![1, 2, 3];
 
    for value in &values {
        println!("read-only: {value}");
    }
 
    for value in &mut values {
        *value *= 10;
    }
 
    println!("after mutation = {:?}", values);
 
    for value in values {
        println!("moved value: {value}");
    }
}

The difference matters. Iterating with &values borrows elements immutably. Iterating with &mut values borrows them mutably. Iterating with values consumes the vector and moves out its elements.

Transforming vectors with iterators and `collect`

One of the most common vector workflows is to borrow or consume a sequence, transform it through iterator adapters, and collect the result into a new vector.

fn main() {
    let values = vec![1, 2, 3, 4, 5];
 
    let squares: Vec<i32> = values.iter().map(|n| n * n).collect();
    println!("squares = {:?}", squares);
}

This style is concise and expressive, but it is still grounded in a simple idea: vectors are a common destination type for collected iterator results. As you learn more Rust, you will find that many transformations naturally end in collect::<Vec<_>>().

Filtering values

Vectors are often used as the result of filtering existing data. You can do this by creating a new vector from an iterator pipeline or by modifying the vector in place with methods like retain.

fn main() {
    let values = vec![1, 2, 3, 4, 5, 6];
 
    let evens: Vec<i32> = values.iter().copied().filter(|n| n % 2 == 0).collect();
    println!("evens = {:?}", evens);
 
    let mut keep_large = vec![3, 8, 1, 12, 5, 20];
    keep_large.retain(|n| *n >= 8);
    println!("retain >= 8 => {:?}", keep_large);
}

Use a new collected vector when the original data should remain unchanged or when you want a transformed output. Use retain when you want to remove elements in place.

Insertion and removal

Vectors support insertion and removal at arbitrary positions, but these operations may need to shift later elements. Appending and popping at the end are usually the cheapest common operations.

fn main() {
    let mut values = vec![10, 20, 40];
    values.insert(2, 30);
    println!("after insert = {:?}", values);
 
    let removed = values.remove(1);
    println!("removed = {}", removed);
    println!("after remove = {:?}", values);
 
    let last = values.pop();
    println!("popped = {:?}", last);
    println!("after pop = {:?}", values);
}

This illustrates an important practical rule. Vec<T> is especially strong when growth and removal mostly happen at the end. If your algorithm frequently pushes and pops at both ends, VecDeque<T> may be a better fit.

`append`, `drain`, and `splice`-style reshaping

Vectors also support operations for moving or removing groups of elements. These become useful once code grows beyond simple push and pop patterns.

fn main() {
    let mut a = vec![1, 2, 3];
    let mut b = vec![4, 5, 6];
 
    a.append(&mut b);
    println!("a = {:?}", a);
    println!("b = {:?}", b);
 
    let mut c = vec![10, 20, 30, 40, 50];
    let drained: Vec<_> = c.drain(1..4).collect();
    println!("c after drain = {:?}", c);
    println!("drained = {:?}", drained);
}

These methods are valuable because they express intent directly. append moves all elements from one vector into another. drain removes a range while yielding the removed items. This is often clearer and safer than manual loops.

Sorting vectors

Because vectors are contiguous, they support efficient in-place sorting. The standard library provides several sorting methods for different needs.

fn main() {
    let mut values = vec![9, 3, 7, 1, 5];
    values.sort();
    println!("sorted = {:?}", values);
 
    let mut words = vec!["pear", "apple", "banana"];
    words.sort_by_key(|word| word.len());
    println!("sorted by length = {:?}", words);
}

sort is stable. sort_unstable may be faster in some cases but does not preserve the relative order of equal elements. Choose based on whether stability matters.

Sorting is one of the clearest examples of when Vec<T> is enough. Many tasks that might initially suggest more specialized structures can be handled perfectly well by gathering data into a vector, sorting it, and then processing it.

Searching vectors

Vectors support several styles of search. For unsorted data, methods based on iteration are often enough. For sorted data, binary search can be very effective.

fn main() {
    let values = vec![10, 20, 30, 40, 50];
 
    println!("contains 30 = {}", values.contains(&30));
    println!("position of 40 = {:?}", values.iter().position(|n| *n == 40));
 
    let sorted = vec![1, 3, 5, 7, 9, 11];
    println!("binary search 7 = {:?}", sorted.binary_search(&7));
    println!("binary search 8 = {:?}", sorted.binary_search(&8));
}

This matters because a vector plus sorting is often a simpler and more efficient solution than introducing a more complex data structure too early.

Deduplication and cleanup patterns

Vectors are also useful for cleanup passes such as deduplication, removing invalid entries, or compacting data. The exact method depends on whether order matters and whether the vector is already sorted.

fn main() {
    let mut values = vec![1, 1, 2, 2, 2, 3, 4, 4];
    values.dedup();
    println!("dedup after sorted input = {:?}", values);
 
    let mut words = vec!["", "alpha", "", "beta", "gamma"];
    words.retain(|word| !word.is_empty());
    println!("non-empty words = {:?}", words);
}

dedup removes consecutive duplicates, so it is most useful on already sorted or otherwise grouped data. retain is a general-purpose in-place filter.

When `Vec<T>` is enough

A vector is enough more often than beginners expect. If your program mainly accumulates items, iterates over them, occasionally indexes them, sorts them, or transforms them into new results, you often do not need anything more specialized.

A common mistake is to jump too quickly to maps, sets, linked structures, or custom containers before the workload actually demands them. Because vectors are contiguous and flexible, they are often both simpler and faster than expected.

For example, a small list of records that is searched occasionally may work perfectly well as a vector. A batch of events waiting to be sorted by timestamp may naturally live in a vector. A function that computes results from a sequence usually returns a vector.

The right question is not whether another data structure has theoretically appealing operations. The right question is whether your actual workload needs those operations enough to justify a more specialized choice.

When `Vec<T>` is not the right choice

Vectors are not ideal for every access pattern. If you need frequent removal from the front, repeated insertion at the beginning, or fast lookup by key, other structures may fit better.

Use VecDeque<T> when front and back queue operations both matter. Use HashMap<K, V> or BTreeMap<K, V> when the dominant operation is lookup by key. Use sets when uniqueness and membership are primary concerns.

This does not make vectors weak. It simply means they are a strong default, not a universal answer.

API design with vectors

One of the most useful design habits is to accept slices and return vectors. This makes your functions flexible on input and clear on output.

fn doubled(values: &[i32]) -> Vec<i32> {
    values.iter().map(|n| n * 2).collect()
}
 
fn main() {
    let array = [1, 2, 3];
    let vector = vec![4, 5, 6];
 
    println!("{:?}", doubled(&array));
    println!("{:?}", doubled(&vector));
}

Taking &[T] instead of &Vec<T> lets callers pass arrays, vectors, and sub-slices. Returning Vec<T> communicates that a new owned sequence has been built. This pairing appears constantly in idiomatic Rust.

A small decision checklist for `Vec<T>`

A vector is usually the right choice when the data is ordered, contiguous, growable, and primarily processed by appending, iterating, transforming, sorting, or occasional indexing.

You should at least consider another collection when key-based lookup, uniqueness, sorted map behavior, or efficient front operations define the workload.

In other words, start with Vec<T> when it matches the shape of the problem. Move away from it only when the problem clearly asks for something else.

A small sandbox project

A tiny Cargo project is enough to experiment with the main vector operations.

[package]
name = "vec-guide"
version = "0.1.0"
edition = "2024"

You can create and run it like this.

cargo new vec-guide
cd vec-guide
cargo run

A minimal src/main.rs could look like this.

fn cleaned_sorted_squares(input: &[i32]) -> Vec<i32> {
    let mut out: Vec<i32> = input
        .iter()
        .copied()
        .filter(|n| *n >= 0)
        .map(|n| n * n)
        .collect();
 
    out.sort();
    out.dedup();
    out
}
 
fn main() {
    let data = vec![3, -1, 2, 3, 4, 2, -5];
    println!("input = {:?}", data);
    println!("output = {:?}", cleaned_sorted_squares(&data));
}

This single example shows several reasons vectors are the default collection: easy ownership of dynamic data, slice-based input APIs, iterator-driven transformation, in-place sorting, and straightforward cleanup.