When you start learning Rust, one of the first and most useful collection types you’ll encounter is the vector. A vector is a growable, heap-allocated list, and it’s one of the most common ways to store a variable number of items in Rust.
While the concept of a dynamic array is familiar to programmers coming from other languages, Rust’s ownership and borrowing rules give Vec<T> some unique characteristics. This guide will walk you through everything you need to know to use vectors effectively.
What is a Vec<T>?
A Vec<T> (pronounced “vector of T”) is a contiguous, growable array type provided by the Rust standard library. Let’s break that down:
- Generic (
<T>): A vector is generic over a typeT. This means you can have a vector ofi32s (Vec<i32>), a vector ofStrings (Vec<String>), or a vector of any other single type. - Growable: Unlike fixed-size arrays, a vector’s size can change. You can add or remove elements at runtime.
- Heap-Allocated: The data a vector holds is stored on the heap. This is what allows it to be resized. The
Vecstruct itself, which contains a pointer to the heap data, its capacity, and its length, can live on the stack. - Contiguous: The elements in a vector are stored next to each other in memory, which allows for fast iteration and CPU cache-friendly access.
Creating Vectors
There are two primary ways to create a vector.
1. Using Vec::new()
This creates a new, empty vector. You’ll often need to declare it as mutable (mut) if you plan to add elements to it.
fn main() {
// We must specify the type, as Rust can't infer it from an empty vector.
let mut v: Vec<i32> = Vec::new();
// Now we can add elements
v.push(5);
v.push(6);
v.push(7);
println!("v contains: {:?}", v); // Output: v contains: [5, 6, 7]
}
2. Using the vec! Macro
The vec! macro is a convenient shorthand for creating a vector with initial elements. This is the most common way to create a vector.
fn main() {
// Rust can infer the type is Vec<i32> from the values.
let v = vec![1, 2, 3];
println!("v contains: {:?}", v); // Output: v contains: [1, 2, 3]
}
Accessing Elements
Accessing elements in a vector can be done in two ways, each with a different approach to safety.
1. Indexing with [] (The “Panic” Method)
You can access an element using square bracket notation. This is simple and direct, but it comes with a risk: if you try to access an index that is out of bounds, your program will panic.
fn main() {
let v = vec![10, 20, 30, 40, 50];
let third: &i32 = &v[2];
println!("The third element is {}", third);
// This will cause a panic!
// let does_not_exist = &v[100];
}
Use this method when you are certain the index is valid (e.g., when iterating from 0..v.len()).
2. Safe Access with .get() (The “Option” Method)
A safer, more idiomatic way to access elements is with the .get() method. Instead of panicking, .get() returns an Option<&T>. It will be Some(&value) if the index is valid, and None if it’s out of bounds.
fn main() {
let v = vec![10, 20, 30, 40, 50];
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
match v.get(100) {
Some(_) => (), // Do nothing if it exists
None => println!("Index 100 is out of bounds, as expected."),
}
}
The Rules of Borrowing and Vectors
Here is where Rust’s safety guarantees shine. You cannot hold a reference to an element in a vector and simultaneously add a new element to it.
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // an immutable borrow
// v.push(4); // This line will NOT compile!
// println!("The first element is: {}", first);
}
Why is this an error? When you push a new element to a vector, it might not have enough capacity on the heap to store it. If that happens, the vector will allocate a new, larger block of memory, copy all the old elements over, and then deallocate the old memory. If this were allowed, the first reference would be pointing to deallocated memory—a dangling pointer! Rust’s borrow checker prevents this entire class of bugs at compile time.
Iterating Over Vectors
There are three main ways to iterate over a vector, depending on whether you need to read, modify, or consume the elements.
1. Iterating by Immutable Reference (&T)
This is for when you just need to read the elements without changing them.
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
}
2. Iterating by Mutable Reference (&mut T)
Use this when you need to modify the elements in place.
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
// Dereference i to modify the value it points to
*i += 50;
}
println!("{:?}", v); // Output: [150, 82, 107]
}
3. Iterating by Value (Taking Ownership)
This consumes the vector. After the loop, the vector and its elements will have been moved, and you can no longer use the original vector variable.
fn main() {
let v = vec![100, 32, 57];
for i in v {
// i is of type i32, not &i32
println!("Got value: {}", i);
}
// println!("{:?}", v); // This would fail! The vector 'v' has been moved.
}
Popular Use Cases
Vectors are the go-to collection for a wide variety of tasks:
- Reading lines from a file: Each line can be stored as a
Stringin aVec<String>. - Storing user input: Reading a series of numbers or words from a user.
- Function arguments and return values: Collecting a list of items to process or returning a list of results.
- Buffers: Acting as a buffer for network I/O or file processing where the amount of data is not known ahead of time.
- Implementing other data structures: Vectors are often used as the underlying storage for more complex data structures like stacks, queues, or even hashmaps.
Conclusion
The Vec<T> is a fundamental building block in Rust. While it functions like a dynamic array from other languages, it’s deeply integrated with Rust’s ownership and borrowing system. By understanding how to create, access, modify, and iterate over vectors safely, you unlock a powerful tool for writing efficient and reliable Rust code. When in doubt and you need a list of things, Vec<T> is almost always the right place to start.