18.2 The Vec<T> Vector Type
A Vec<T>—often called a “vector”—is a dynamic, growable list stored contiguously on the heap. It provides fast indexing, can change size at runtime, and manages its memory automatically. This is conceptually similar to std::vector in C++ or a manually sized, dynamically allocated array in C, but with Rust’s safety guarantees and automated cleanup.
18.2.1 Creating a Vector
There are several ways to create a new vector:
-
Empty Vector:
let v: Vec<i32> = Vec::new(); // If the type is omitted, Rust attempts type inference. -
Using the
vec!Macro:let v1: Vec<i32> = vec![]; // Empty let v2 = vec![1, 2, 3]; // Infers Vec<i32> let v3 = vec![0; 5]; // 5 zeros of type i32 -
From Iterators or Other Data:
let v: Vec<_> = (1..=5).collect(); // [1, 2, 3, 4, 5] let slice: &[i32] = &[10, 20, 30]; let v2 = slice.to_vec(); let array = [4, 5, 6]; let v3 = Vec::from(array); -
Vec::with_capacityfor Pre-allocation:let mut v = Vec::with_capacity(10); for i in 0..10 { v.push(i); }This avoids multiple reallocations if you know roughly how many items you will store.
18.2.2 Properties and Memory Management
Under the hood, a Vec<T> maintains:
- A pointer to a heap-allocated buffer,
- A
len(the current number of elements), - A
capacity(the total number of elements that can fit before a reallocation is needed).
When you remove elements, the length decreases but the capacity remains. You can call shrink_to_fit() if you want to reduce capacity:
let mut v = vec![1, 2, 3, 4, 5];
v.pop();
v.shrink_to_fit(); // Release spare capacity
Rust’s borrowing rules prevent dangling references and out-of-bounds access. If you try to use v[index] with an invalid index, the program panics at runtime. Meanwhile, v.get(index) returns None if the index is out of range.
18.2.3 Basic Methods
push(elem): Appends an element (reallocation may occur).pop(): Removes the last element and returns it, orNoneif empty.get(index): ReturnsOption<&T>safely.- Indexing (
[]): Returns&T, panics if the index is invalid. len(): Returns the current number of elements.insert(index, elem): Inserts an element at a specific position, shifting subsequent elements.remove(index): Removes and returns the element at the given position, shifting elements down.
18.2.4 Accessing Elements
let v = vec![10, 20, 30];
// Panics on invalid index
println!("First element: {}", v[0]);
// Safe access using `get`
if let Some(value) = v.get(1) {
println!("Second element: {}", value);
}
// `pop` removes from the end
let mut v2 = vec![1, 2, 3];
if let Some(last) = v2.pop() {
println!("Popped: {}", last);
}
18.2.5 Iteration Patterns
// Immutable iteration
let v = vec![1, 2, 3];
for val in &v {
println!("{}", val);
}
// Mutable iteration
let mut v2 = vec![10, 20, 30];
for val in &mut v2 {
*val += 5;
}
// Consuming iteration (v3 is moved)
let v3 = vec![100, 200, 300];
for val in v3 {
println!("{}", val);
}
18.2.6 Handling Mixed Data
All elements in a Vec<T> must be of the same type. If you need different types, consider:
- An
enumthat encompasses all possible variants. - Trait objects (e.g.,
Vec<Box<dyn Trait>>) for runtime polymorphism.
For example, using an enum:
enum Value { Integer(i32), Float(f64), Text(String), } fn main() { let mut mixed = Vec::new(); mixed.push(Value::Integer(42)); mixed.push(Value::Float(3.14)); mixed.push(Value::Text(String::from("Hello"))); for val in &mixed { match val { Value::Integer(i) => println!("Integer: {}", i), Value::Float(f) => println!("Float: {}", f), Value::Text(s) => println!("Text: {}", s), } } }
Using trait objects adds overhead due to dynamic dispatch and extra heap allocations. Choose the approach that best meets your performance and design needs.
18.2.7 Summary: Vec<T> vs. C
In C, you might manually manage an array with malloc/realloc/free, tracking capacity yourself. Rust’s Vec<T> automates these tasks, prevents out-of-bounds access, and reclaims memory when the vector goes out of scope. This significantly reduces memory-management errors while still allowing fine-grained performance tuning (e.g., pre-allocation via with_capacity).