Vectors (Vec<T>)
Learn to use Rust's Vec collection type for dynamic arrays including creating and modifying elements safe access iteration and storing heterogeneous data with enums
What Vec<T> Is and Why You Need It
A Vec<T> is Rust's growable array type. It stores a sequence of values of the same type T in a single block of contiguous memory on the heap. Because the data sits next to each other, iterating through a vector is extremely fast — the CPU can pull values in order with minimal memory lookups.
The critical difference between a vector and an array ([T; N]) or tuple is that a vector's size does not need to be known at compile time. You can add elements at runtime until your program runs out of memory. This makes Vec<T> the default collection whenever you need to accumulate items whose count you cannot predict upfront — reading lines from a file, building a list of search results, storing user input, and thousands of other real-world tasks.
If you have worked with Python lists or JavaScript arrays, you already understand the role a vector plays. Rust's version enforces type safety: every element in a Vec<i32> is an i32, and the compiler guarantees it.
Creating a Vector
Rust offers several ways to create a vector. Each is useful in a different situation.
Starting from nothing requires a type annotation if the compiler cannot infer the element type later:
let mut numbers: Vec<i32> = Vec::new();
The mut keyword is necessary because you will want to add elements. An immutable empty vector is rarely useful.
The vec! macro is the most convenient tool for creating a vector with initial values. It accepts a list of elements separated by commas:
let primes = vec![2, 3, 5, 7, 11];
Rust infers the type from the values you provide, so primes becomes a Vec<i32>. The macro also supports a repetition pattern that creates a vector filled with copies of a single value:
let zeros = vec![0; 100]; // 100 zeros
This form can be more efficient than repeatedly pushing because it allocates exactly the right amount of memory in one shot.
From an existing array or slice using the into conversion:
let array = [1, 2, 3];
let vec_from_array: Vec<i32> = array.into();
// or using the From trait
let also_from_array = Vec::from([1, 2, 3]);
With a pre-allocated capacity when you already know roughly how many elements you will push:
let mut buffer: Vec<u8> = Vec::with_capacity(1024);
for byte in source {
buffer.push(byte);
}
Preallocating Capacity:
Every time a vector grows beyond its current capacity, it must allocate a new, larger block of memory and copy all existing elements over. If you know the final size ahead of time, Vec::with_capacity avoids those reallocations entirely. The len() method tells you how many elements the vector currently holds, while capacity() reports the total space available before another allocation will be triggered.
Adding and Removing Elements
push and pop turn a vector into an efficient stack. Both operate on the end of the sequence, which is an O(1) operation on average.
let mut stack = Vec::new();
stack.push(10);
stack.push(20);
stack.push(30);
let top = stack.pop(); // Returns Some(30)
// stack now contains [10, 20]
pop returns an Option<T> — None if the vector was empty. This makes it safe to call without checking the length beforehand.
The vector takes ownership of any value you push, unless that value's type implements the Copy trait (like integers, booleans, or floats). For non-Copy types such as String, the original variable becomes unusable:
let name = String::from("Alice");
let mut names: Vec<String> = Vec::new();
names.push(name);
// println!("{}", name); // error: value borrowed here after move
This behavior protects you from accidentally using data that has been logically transferred into the collection.
To insert or remove an element at an arbitrary position, use insert and remove. These methods are O(n) because all elements after the affected index must be shifted left or right.
let mut v = vec![1, 2, 3];
v.insert(1, 99); // [1, 99, 2, 3]
let removed = v.remove(2); // removes 2, returns it; vector is now [1, 99, 3]
extend lets you append all elements from an iterator in one call. It is typically faster than pushing individually because it can reserve space more intelligently.
let mut v = vec![1, 2];
v.extend([3, 4, 5]);
// v is now [1, 2, 3, 4, 5]
Accessing Elements Safely
Rust provides two ways to retrieve an element by its index, and choosing between them is a deliberate safety decision.
Direct indexing with v[i] gives you an immutable reference (or mutable if the vector is &mut and you write &mut v[i]). It is short and fast, but it will make your program panic if i is out of bounds.
let v = vec![10, 20, 30];
let second = v[1]; // 20
// let missing = v[100]; // would panic at runtime
The get method returns an Option<&T>. You then handle the possibility of a missing index with a match, if let, or unwrap_or.
let v = vec![10, 20, 30];
match v.get(5) {
Some(value) => println!("Found: {}", value),
None => println!("Index out of range"),
}
Indexing Panics on Out-of-Bounds Access:
Using v[i] when i >= v.len() causes an immediate panic — the program crashes. The compiler does not check index bounds at compile time. In any situation where the index comes from user input, network data, or any source you do not fully control, prefer get.
A mutable version get_mut works the same way for changing an element in place:
if let Some(val) = v.get_mut(0) {
*val += 50;
}
Iterating Over Vectors
Iteration is the most common way to process every element. Rust's for loop works with three different borrowing patterns, and each one tells the compiler exactly what kind of access you need.
Immutable iteration borrows each element as &T. The original vector remains fully usable afterward.
let numbers = vec![1, 2, 3];
for n in &numbers {
println!("{}", n);
}
println!("Still have {} numbers", numbers.len()); // numbers is still valid
Mutable iteration borrows each element as &mut T. You must dereference the value with * to modify it.
let mut numbers = vec![1, 2, 3];
for n in &mut numbers {
*n *= 10;
}
// numbers is now [10, 20, 30]
Consuming iteration uses into_iter (implicitly when you write for n in vec_name without a reference). The vector is destroyed after the loop, and each element is moved out.
let names = vec![String::from("Alice"), String::from("Bob")];
for name in names {
println!("{}", name);
}
// println!("{:?}", names); // error: value borrowed here after move
Iteration and Borrow Checker Interactions:
If you hold an immutable reference to an element (e.g., through indexing or a borrow returned by get), you cannot call push on the same vector. The borrow checker enforces this because a push might cause a reallocation, which would move the memory and invalidate the reference. This is one of the most common compile errors beginners encounter with vectors — the fix is usually to clone the value or restructure the code so the borrow ends before the mutation.
Storing Different Types with Enums
A vector can only hold elements of a single concrete type. When you need a collection that contains values of different types, you wrap them in an enum.
enum CellValue {
Integer(i32),
Text(String),
Boolean(bool),
}
let row = vec![
CellValue::Integer(42),
CellValue::Text(String::from("hello")),
CellValue::Boolean(true),
];
for cell in &row {
match cell {
CellValue::Integer(n) => println!("Integer: {}", n),
CellValue::Text(s) => println!("Text: {}", s),
CellValue::Boolean(b) => println!("Boolean: {}", b),
}
}
This pattern is common in spreadsheet applications, configuration file parsers, and anywhere you read mixed data from an external source. The compiler still guarantees exhaustiveness — it will refuse to compile if you forget to handle one of the variants inside the match.
Ownership and Vectors
The way ownership interacts with vectors deserves explicit attention because it causes confusion even for developers who understand ownership in isolation.
When you push a value onto a vector, the vector becomes the owner of that value. If the value's type does not implement Copy, the original binding cannot be used afterward.
let s = String::from("hello");
let mut v = Vec::new();
v.push(s);
// s is no longer valid here
Similarly, passing a vector to a function by value transfers ownership, and the caller loses access. To let the caller keep using the vector, pass a reference (&Vec<T> or &[T] for a slice).
fn sum(numbers: &Vec<i32>) -> i32 {
numbers.iter().sum()
}
let v = vec![1, 2, 3];
let total = sum(&v);
println!("Vector after calling sum: {:?}", v); // v is still usable
Borrow rules also mean you cannot mutate a vector through one path while reading it through another. This includes seemingly safe patterns like modifying a vector inside a loop that iterates over it. The compiler will stop you with a clear error message — that is intentional. It is protecting you from iterator invalidation bugs that are common in languages without a borrow checker.
Safe Access Patterns Compile Without Surprises:
If your code compiles, you can be confident that you are not accidentally using a moved value, indexing out of bounds (when using get), or mutating while iterating. The compiler checks these invariants at compile time. The get method, in particular, turns a potential runtime panic into a forced compile-time decision to handle the None case.
Summary
Vectors sit at the center of most Rust programs that work with lists of data. Their power comes from three properties that work together: they grow on demand, they keep elements packed tightly in memory for fast iteration, and the ownership system prevents the use-after-move and iterator-invalidation bugs that plague collections in other systems languages.
The most important habit to build right now is preferring get over direct indexing whenever the index is not a compile-time constant you fully trust. The moment you internalize that pattern, you stop writing vector code that can panic unexpectedly.