Vectors

A vector (Vec<T>) is Rust’s growable, heap-allocated list type: an array that can shrink and grow at runtime. It is the collection you reach for by default whenever you need an ordered, resizable sequence of values of the same type. Because Rust has no garbage collector, understanding how a Vec owns its data and how borrowing it works is essential to writing code that actually compiles — and to understanding the borrow checker errors you will eventually hit.

What Is a Vector?

Rust’s built-in array type, [T; N], has a fixed size baked into its type: a [i32; 3] can never hold 4 elements. A Vec<T> solves this by storing its elements in a buffer on the heap instead of inline on the stack. What lives on the stack is just three words: a pointer to the heap buffer, a length (how many elements are currently stored), and a capacity (how many elements the buffer can hold before it needs to reallocate).

Think of a vector like a row of lockers you rent from a storage company. You start by renting a small block of lockers (the capacity). As you fill them (the length grows), eventually you run out of room. At that point the storage company finds you a bigger block elsewhere in the building, copies everything over, and gives you a new starting locker number (a new pointer). Anyone who wrote down the old locker number now has a stale reference — which is exactly the situation Rust’s borrow checker exists to prevent.

Because a Vec<T> owns its heap buffer, it follows the same ownership rules as any other value: when the variable holding it goes out of scope, Rust automatically frees the buffer (calling Drop on every element first). There is no manual free and no garbage collector pause — just deterministic cleanup tied to scope.

Syntax

A vector can be created empty, from a list of values, or with a pre-reserved capacity. Once you have one, most operations come from methods rather than special syntax:

let v: Vec<T> = Vec::new();
let v = vec![v1, v2, v3];
let v: Vec<T> = Vec::with_capacity(10);

v.push(value);
v.pop();
v[index];
v.get(index);
v.len();
v.is_empty();
for item in &v { ... }
for item in &mut v { ... }
for item in v { ... }
Form / Method Meaning
Vec::new() Creates an empty vector with zero capacity; nothing is allocated yet.
vec![a, b, c] Macro that creates a vector already populated with the given values.
Vec::with_capacity(n) Allocates room for n elements up front to avoid repeated reallocation.
v.push(x) Appends x to the end, growing the buffer if needed.
v.pop() Removes and returns the last element as Option<T>, or None if empty.
v[i] Indexing; panics at runtime if i is out of bounds.
v.get(i) Safe access; returns Option<&T> instead of panicking.
v.len() / v.is_empty() Current number of elements, and whether that number is zero.

Examples

Example 1: Summing a Vector

fn main() {
    let numbers = vec![10, 20, 30, 40];
    let mut sum = 0;
    for n in &numbers {
        sum += n;
    }
    println!("Numbers: {:?}", numbers);
    println!("Sum: {}", sum);
}

Output:

Numbers: [10, 20, 30, 40]
Sum: 100

The macro vec![10, 20, 30, 40] builds a Vec<i32> in one step. The for n in &numbers loop borrows each element instead of taking ownership, which is why numbers can still be printed afterward in the {:?} debug format — if we had written for n in numbers without the &, the loop would have moved (consumed) the vector, and the later println! would fail to compile.

Example 2: Push, Pop, and Safe Access

fn main() {
    let mut stack: Vec<i32> = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);

    println!("Stack after pushes: {:?}", stack);

    match stack.pop() {
        Some(top) => println!("Popped: {}", top),
        None => println!("Stack was empty"),
    }

    println!("Stack after pop: {:?}", stack);

    match stack.get(5) {
        Some(value) => println!("Value at index 5: {}", value),
        None => println!("No value at index 5"),
    }
}

Output:

Stack after pushes: [1, 2, 3]
Popped: 3
Stack after pop: [1, 2]
No value at index 5

This example uses a vector as a stack, which is one of its most common roles: push adds to the end and pop removes from the end, both in constant amortized time. Notice that pop returns Option<i32> rather than the raw value, because popping an empty vector is a normal situation, not an error — and get(5) returns None instead of panicking, since index 5 does not exist in a two-element vector.

Example 3: A Vector of Structs

struct Task {
    name: String,
    done: bool,
}

fn main() {
    let mut tasks = vec![
        Task { name: String::from("Write lesson"), done: false },
        Task { name: String::from("Review code"), done: false },
        Task { name: String::from("Publish"), done: false },
    ];

    if let Some(first) = tasks.first_mut() {
        first.done = true;
    }

    for (index, task) in tasks.iter().enumerate() {
        let status = if task.done { "done" } else { "pending" };
        println!("{}: {} ({})", index, task.name, status);
    }
}

Output:

0: Write lesson (done)
1: Review code (pending)
2: Publish (pending)

Vectors of custom structs are extremely common in real programs. tasks.first_mut() returns Option<&mut Task>, letting us mutate the first element in place without removing it. tasks.iter().enumerate() then gives us both an index and a shared reference to each Task for read-only display. Each Task owns its own String, and that whole tree of ownership is dropped automatically when tasks goes out of scope at the end of main.

How Ownership and Borrowing Work with Vectors

A Vec<T> does not implement Copy, so assigning it to another variable or passing it by value moves it — the original binding becomes invalid, and the compiler will refuse to let you use it again. This matters constantly in practice, so functions that only need to read a vector should almost always take a reference instead of taking ownership:

fn print_all(items: &Vec<i32>) {
    for item in items {
        print!("{} ", item);
    }
    println!();
}

fn main() {
    let numbers = vec![1, 2, 3];
    print_all(&numbers);
    println!("Still usable: {:?}", numbers);
}

Output:

1 2 3 
Still usable: [1, 2, 3]

Because print_all takes &Vec<i32> rather than Vec<i32>, calling it only lends the data temporarily; ownership never leaves main, so numbers is still valid on the next line. This is the core mental model: a Vec has exactly one owner at a time, and everyone else can only borrow it, either immutably (read-only, any number of borrows at once) or mutably (exclusive, only one at a time).

This is also why the compiler is strict about mutating a vector while a reference to its contents is alive. Pushing an element can force the vector to outgrow its current capacity, which means Rust allocates a new, larger buffer, copies every existing element into it, and frees the old buffer. Any reference that pointed into the old buffer would now point at freed memory. Rather than risk that at runtime, the borrow checker rejects the code at compile time — you cannot hold a live reference into a vector across a call that might reallocate it.

Common Mistakes

Mistake 1: Using a Value After It’s Moved

Passing a Vec to a function by value transfers ownership into that function. Once the function returns, the vector is gone from the caller’s perspective — using it afterward is a compile error, not a runtime bug:

fn consume(v: Vec<i32>) {
    println!("Consumed: {:?}", v);
}

fn main() {
    let numbers = vec![1, 2, 3];
    consume(numbers);
    println!("Still have it: {:?}", numbers);
}

This fails with error[E0382]: borrow of moved value: 'numbers', because consume(numbers) moved the vector into the function, and the final println! tries to read a binding that no longer owns anything. The fix is to borrow instead of moving, unless the function genuinely needs ownership:

fn consume(v: &Vec<i32>) {
    println!("Consumed: {:?}", v);
}

fn main() {
    let numbers = vec![1, 2, 3];
    consume(&numbers);
    println!("Still have it: {:?}", numbers);
}

Output:

Consumed: [1, 2, 3]
Still have it: [1, 2, 3]

Mistake 2: Holding a Reference While Mutating

As explained above, a reference into a vector cannot stay alive across a mutation that might reallocate the buffer:

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("First element: {}", first);
}

This fails with error[E0502]: cannot borrow 'v' as mutable because it is also borrowed as immutable, because first is still in use on the last line, after the push. The simplest fix, when the element type is Copy (like i32), is to copy the value out instead of borrowing it:

fn main() {
    let mut v = vec![1, 2, 3];
    let first = v[0];
    v.push(4);
    println!("First element: {}", first);
    println!("Vector now: {:?}", v);
}

Output:

First element: 1
Vector now: [1, 2, 3, 4]

Mistake 3: Index Out of Bounds Panics

Unlike a borrow-checker violation, indexing past the end of a vector compiles fine — it is a runtime footgun, not a compile-time one:

fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[5]);
}

This program compiles without any warning, but crashes the moment it runs, before anything is printed, with a message like thread 'main' panicked at ...: index out of bounds: the len is 3 but the index is 5. Whenever the index is not guaranteed to be in range — for example, it came from user input or a calculation — use .get() and handle both cases explicitly:

fn main() {
    let v = vec![1, 2, 3];
    match v.get(5) {
        Some(value) => println!("Value: {}", value),
        None => println!("No value at index 5"),
    }
}

Output:

No value at index 5

Best Practices

  • Prefer &[T] (a slice) over &Vec<T> for function parameters that only read the data — a slice parameter also accepts arrays and other slices, not just vectors.
  • Use Vec::with_capacity(n) when you know roughly how many elements you will push; it avoids repeated reallocation and copying as the vector grows.
  • Reach for .get(i) instead of v[i] whenever the index is not provably in range, and handle the None case instead of letting the program panic.
  • Iterate with &v (or .iter()) when you only need to read elements, and with &mut v (or .iter_mut()) when you need to modify them in place — avoid consuming a vector with a plain for x in v loop unless you genuinely no longer need it afterward.
  • Use .iter().enumerate() instead of manually tracking an index counter when you need both the position and the value.
  • Remember that vec.pop(), vec.first(), and vec.last() all return Option<T> or Option<&T> because an empty vector is a normal, expected case, not an error.

Practice Exercises

  • Write a program that builds a Vec<i32> from the numbers 1 through 10 with a loop and push, then prints the average as a floating-point number.
  • Write a function fn largest(numbers: &[i32]) -> Option<i32> that returns the largest value in a slice, or None if the slice is empty. Call it with both an empty and a non-empty vector and print both results.
  • Create a Vec of a custom struct representing a product (name and price). Use .iter_mut() to apply a 10% discount to every product’s price, then print the updated list.

Summary

  • Vec<T> is a growable, heap-allocated list; the stack only holds a pointer, a length, and a capacity.
  • A vector owns its elements: when it goes out of scope, every element is dropped and the buffer is freed automatically.
  • Assigning or passing a Vec by value moves it; pass &Vec<T> or, better, &[T] when a function only needs to read it.
  • You can hold one mutable reference or many immutable references into a vector at a time, never both — this rule exists because pushing can reallocate the buffer and invalidate old references.
  • v[i] panics on an out-of-range index at runtime; v.get(i) returns Option and never panics.
  • Prefer safe, idiomatic access patterns (.get(), .iter(), .iter_mut(), .enumerate()) over manual indexing wherever possible.