Rust 29 🦀 Vector Collection (Vec) Operations and Storage
A Vec<T> is a growable array. It is the most common collection in Rust, and it appears in nearly every program. It stores its elements in a contiguous block of memory on the heap, and it tracks three things: a pointer to the allocation, a length, and a capacity. The length is how many elements are present. The capacity is how many can fit before reallocation. The distinction between the two is the key to understanding how a vector grows, when it reallocates, and why some operations are amortized constant time while others are linear.
This chapter covers Vec<T> in full. You will learn how to create, grow, shrink, and access vectors, how indexing differs from get, how iteration works and how it interacts with ownership, and how the vector manages its memory. You will also see the patterns that keep vector operations efficient and the pitfalls that produce panics, borrow checker errors, and unexpected copies.
Key point: A Vec<T> owns its elements. It allocates them on the heap, and it frees them when the vector is dropped. Accessing an element by index (v[i]) panics if the index is out of bounds. Accessing it with get returns an Option and never panics. Iterating by value consumes the vector, by reference borrows it, and by mutable reference allows modification in place.
Why Vec exists
The fixed-size problem. An array in Rust, [T; N], has a size known at compile time. It cannot grow or shrink. Many programs need a collection whose size is determined at runtime: the lines of a file, the results of a query, the elements read from a stream. Vec<T> provides a dynamically sized array that grows as needed.
The memory layout problem. A vector stores its elements contiguously. This means that index access is a single pointer offset, iteration is cache-friendly, and the elements can be passed to APIs that expect a slice. A linked list would provide growth but lose contiguity. For the common case—append, index, iterate—contiguous storage is faster.
The ownership problem. A Vec<T> owns its elements. When the vector is dropped, every element is dropped. When an element is removed, it is dropped. This is automatic, and it is the same ownership model that applies everywhere in Rust. There is no manual free and no garbage collector.
The capacity problem. Growing a vector means allocating a larger block, copying the elements, and freeing the old block. Doing this on every push would be O(n²). Instead, the vector allocates more than it needs. The capacity grows geometrically, so the number of reallocations is logarithmic in the number of elements. Each push is amortized O(1).
The slice problem. A &[T] is a view into a contiguous sequence. A &Vec<T> coerces to &[T], which means functions that accept a slice can be called with a vector. This is why most APIs take &[T] rather than &Vec<T>—the slice is more general and does not constrain the caller to a vector.
a. Creating, growing, and accessing vectors
A vector is created with Vec::new or the vec! macro.
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
let v2 = vec![1, 2, 3];
let v3 = vec![0; 5]; // [0, 0, 0, 0, 0]
Vec::new creates an empty vector with no allocation. The type annotation is required because the compiler cannot infer the element type from an empty vector. vec![1, 2, 3] creates a vector with three elements. vec![0; 5] creates a vector with five copies of zero.
Vec::with_capacity allocates space for a given number of elements without adding any.
let mut v = Vec::with_capacity(100);
println!("{}", v.capacity()); // 100
println!("{}", v.len()); // 0
This is useful when the number of elements is known in advance. It avoids the reallocations that would otherwise occur as the vector grows.
Elements are accessed by index or with get.
let v = vec![10, 20, 30];
let second = v[1]; // 20, panics if out of bounds
let third = v.get(2); // Some(&30)
let tenth = v.get(9); // None
The index operator returns the element by value (or by reference, depending on context) and panics if the index is out of bounds. The get method returns Option<&T> and never panics. Use get when the index might be out of bounds; use indexing when it should be.
Elements can be modified through a mutable reference:
let mut v = vec![1, 2, 3];
v[0] = 10;
The first and last methods return Option<&T>:
let v = vec![1, 2, 3];
println!("{:?}", v.first()); // Some(&1)
println!("{:?}", v.last()); // Some(&3)
The len method returns the number of elements, and is_empty returns whether the vector is empty.
b. Removing, inserting, and the capacity model
Elements can be added at the end with push and removed from the end with pop.
let mut v = vec![1, 2, 3];
v.push(4); // [1, 2, 3, 4]
let last = v.pop(); // Some(4), v is now [1, 2, 3]
The pop method returns Option<T>, which is None when the vector is empty.
Elements can be inserted and removed at arbitrary positions with insert and remove.
let mut v = vec![1, 2, 3];
v.insert(1, 10); // [1, 10, 2, 3]
let removed = v.remove(1); // 10, v is [1, 2, 3]
Both operations shift the elements after the position, which makes them O(n). The insert shifts elements to the right to make room; the remove shifts them to the left to close the gap.
The swap_remove method removes an element by replacing it with the last element. This is O(1) but does not preserve order.
let mut v = vec![1, 2, 3, 4];
let removed = v.swap_remove(1); // 2, v is [1, 4, 3]
The last element (4) is moved into position 1, and the last slot is removed. Use swap_remove when order does not matter.
The capacity grows automatically as elements are pushed. When the length exceeds the capacity, the vector allocates a new block, typically twice the old capacity, copies the elements, and frees the old block.
let mut v = Vec::new();
println!("{}", v.capacity()); // 0
v.push(1);
println!("{}", v.capacity()); // 4 (or 1, implementation-defined)
v.push(2);
v.push(3);
v.push(4);
v.push(5);
println!("{}", v.capacity()); // 8
The exact growth factor is not guaranteed, but it is geometric. This is what makes push amortized O(1).
The reserve and reserve_exact methods allocate additional capacity:
let mut v = vec![1, 2, 3];
v.reserve(10); // capacity >= 13
v.reserve_exact(10); // capacity == 13
reserve may allocate more than requested; reserve_exact allocates exactly. Use reserve for performance, reserve_exact for memory control.
The shrink_to_fit method reduces the capacity to the length:
let mut v = Vec::with_capacity(100);
v.push(1);
v.shrink_to_fit();
println!("{}", v.capacity()); // 1
This frees unused memory. It is useful after a vector has been built and will not grow further.
c. Iteration and ownership
A vector can be iterated by reference, by mutable reference, or by value. Each form has different ownership semantics.
By reference borrows the elements. The vector is not consumed.
let v = vec![1, 2, 3];
for x in &v {
println!("{}", x); // x is &i32
}
println!("{:?}", v); // v is still valid
By mutable reference borrows the elements mutably. The elements can be modified.
let mut v = vec![1, 2, 3];
for x in &mut v {
*x *= 2;
}
println!("{:?}", v); // [2, 4, 6]
By value consumes the vector. The elements are moved out, and the vector cannot be used afterward.
let v = vec![String::from("a"), String::from("b")];
for s in v {
println!("{}", s); // s is String
}
// println!("{:?}", v); // error: v was moved
The into_iter method is the explicit form of by-value iteration. iter is by-reference, and iter_mut is by-mutable-reference.
let v = vec![1, 2, 3];
let sum: i32 = v.iter().sum();
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let evens: Vec<i32> = v.into_iter().filter(|x| x % 2 == 0).collect();
The iter method returns an iterator of &T, iter_mut returns &mut T, and into_iter returns T. The collect method gathers the iterator into a new collection.
The borrow checker prevents modifying a vector while iterating it by reference:
let mut v = vec![1, 2, 3];
for x in &v {
// v.push(4); // error: cannot borrow v as mutable
println!("{}", x);
}
The immutable borrow from the loop conflicts with the mutable borrow from push. The fix is to collect the changes and apply them after the loop, or to use indices instead of iteration.
Complete Example Session
// ============================================
// PART 1: CREATING A VECTOR
// ============================================
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
let v2 = vec![1, 2, 3];
let v3 = vec![0; 5];
// ============================================
// PART 2: WITH CAPACITY
// ============================================
let mut v = Vec::with_capacity(100);
println!("capacity: {}", v.capacity()); // 100
println!("length: {}", v.len()); // 0
// ============================================
// PART 3: INDEXING VS GET
// ============================================
let v = vec![10, 20, 30];
let second = v[1]; // 20
let third = v.get(2); // Some(&30)
let tenth = v.get(9); // None
// ============================================
// PART 4: PUSH AND POP
// ============================================
let mut v = vec![1, 2, 3];
v.push(4); // [1, 2, 3, 4]
let last = v.pop(); // Some(4)
let empty = v.pop(); // Some(3) or None depending on state
// ============================================
// PART 5: INSERT AND REMOVE
// ============================================
let mut v = vec![1, 2, 3];
v.insert(1, 10); // [1, 10, 2, 3]
let removed = v.remove(1); // 10
// ============================================
// PART 6: SWAP_REMOVE
// ============================================
let mut v = vec![1, 2, 3, 4];
let removed = v.swap_remove(1); // 2
// v is now [1, 4, 3]
// ============================================
// PART 7: ITERATION BY REFERENCE
// ============================================
let v = vec![1, 2, 3];
for x in &v {
println!("{}", x);
}
// ============================================
// PART 8: ITERATION BY MUTABLE REFERENCE
// ============================================
let mut v = vec![1, 2, 3];
for x in &mut v {
*x *= 2;
}
println!("{:?}", v); // [2, 4, 6]
// ============================================
// PART 9: ITERATION BY VALUE
// ============================================
let v = vec![String::from("a"), String::from("b")];
for s in v {
println!("{}", s);
}
// v is moved
// ============================================
// PART 10: COLLECT AND TRANSFORM
// ============================================
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<i32> = v.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 10)
.collect();
println!("{:?}", evens); // [20, 40]
The ten parts covered creating a vector, with_capacity, indexing versus get, push and pop, insert and remove, swap_remove, iteration by reference, iteration by mutable reference, iteration by value, and collect with transformations.
Quick Reference
Creation
| Method | Purpose |
|---|---|
Vec::new() | Empty vector |
vec![1, 2, 3] | Vector with elements |
vec![0; 5] | Vector with repeated value |
Vec::with_capacity(n) | Pre-allocated |
Vec::from([1, 2, 3]) | From an array |
Access
| Method | Returns |
|---|---|
v[i] | Element, panics if out of bounds |
v.get(i) | Option<&T> |
v.first() | Option<&T> |
v.last() | Option<&T> |
v.len() | Number of elements |
v.is_empty() | Boolean |
v.capacity() | Allocated capacity |
Modification
| Method | Effect | Complexity |
|---|---|---|
v.push(x) | Add to end | Amortized O(1) |
v.pop() | Remove from end | O(1) |
v.insert(i, x) | Insert at position | O(n) |
v.remove(i) | Remove at position | O(n) |
v.swap_remove(i) | Remove, swap with last | O(1) |
v.clear() | Remove all | O(n) |
v.truncate(n) | Keep first n | O(n) |
Capacity
| Method | Purpose |
|---|---|
v.reserve(n) | Allocate at least n more |
v.reserve_exact(n) | Allocate exactly n more |
v.shrink_to_fit() | Reduce capacity to length |
v.capacity() | Current capacity |
Iteration
| Method | Yields |
|---|---|
v.iter() | &T |
v.iter_mut() | &mut T |
v.into_iter() | T |
for x in &v | &T |
for x in &mut v | &mut T |
for x in v | T |
Best Practices
✅ Do This:
// Use vec! for literals
let v = vec![1, 2, 3]; // ✅
// Use with_capacity when size is known
let mut v = Vec::with_capacity(1000); // ✅
// Use get when the index might be out of bounds
if let Some(x) = v.get(10) { } // ✅
// Use iter for reference iteration
for x in v.iter() { } // ✅
// Use iter_mut for in-place modification
for x in v.iter_mut() { *x += 1; } // ✅
// Use swap_remove when order does not matter
v.swap_remove(0); // ✅
// Use slices for function parameters
fn process(data: &[i32]) { } // ✅
❌ Don’t Do This:
// Don't index without checking bounds
let x = v[100]; // panics if v has fewer than 101 elements // ❌
// Don't push in a loop without reserving
for i in 0..1000 { v.push(i); } // reallocates multiple times // ⚠️
// Don't insert at the front of a large vector
v.insert(0, x); // O(n) every time // ⚠️
// Don't modify a vector while iterating by reference
for x in &v { v.push(*x); } // borrow error // ❌
// Don't use &Vec<T> for function parameters
fn process(data: &Vec<i32>) { } // use &[i32] instead // ⚠️
// Don't forget into_iter consumes the vector
let sum: i32 = v.into_iter().sum();
// println!("{:?}", v); // error: v was moved // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Index out of bounds | Used v[i] without checking | Use v.get(i) |
| Reallocation cost | Push without with_capacity | Pre-allocate |
| Borrow error in loop | Modifying while iterating | Collect changes first |
| Vector moved | Used into_iter | Use iter for borrowing |
| Slow insert at front | insert(0, x) is O(n) | Use VecDeque |
| Capacity larger than needed | Push then never shrink | shrink_to_fit |
| Type annotation required | Empty Vec::new() | Annotate Vec<T> |
Real-World Examples
1. Collect from Iterator
let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
2. Filter and Collect
let evens: Vec<i32> = numbers.iter().filter(|x| *x % 2 == 0).copied().collect();
3. Sum
let total: i32 = v.iter().sum();
4. Find
let first_even = v.iter().find(|x| *x % 2 == 0);
5. Sort
let mut v = vec![3, 1, 2];
v.sort(); // [1, 2, 3]
6. Reverse
let mut v = vec![1, 2, 3];
v.reverse(); // [3, 2, 1]
7. Deduplicate
let mut v = vec![1, 1, 2, 3, 3];
v.dedup(); // [1, 2, 3]
8. Contains
if v.contains(&42) { }
9. Extend
let mut v = vec![1, 2];
v.extend([3, 4]); // [1, 2, 3, 4]
10. Split Off
let mut v = vec![1, 2, 3, 4, 5];
let tail = v.split_off(3); // v is [1, 2, 3], tail is [4, 5]
Visual
Vector Memory Layout
┌─────────────────────────────────────────────────────────────┐
│ Vec<T> │
│ ┌─────────┐ │
│ │ ptr │ ──▶ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ len: 3 │ │ 1 │ 2 │ 3 │ │ │ │ │
│ │ cap: 6 │ └─────┴─────┴─────┴─────┴─────┴─────┘ │
│ └─────────┘ ◀── length ──▶ │
│ ◀────────── capacity ──────────────────▶ │
│ │
│ len: number of initialized elements │
│ cap: number of elements that fit before reallocation │
│ │
└─────────────────────────────────────────────────────────────┘
Growth and Reallocation
┌─────────────────────────────────────────────────────────────┐
│ push(1): capacity 0 → 4 │
│ ┌───┐ │
│ │ 1 │ len: 1, cap: 4 │
│ └───┘ │
│ │
│ push(2), push(3), push(4): no reallocation │
│ ┌───┬───┬───┬───┐ │
│ │ 1 │ 2 │ 3 │ 4 │ len: 4, cap: 4 │
│ └───┴───┴───┴───┘ │
│ │
│ push(5): capacity 4 → 8 (reallocate and copy) │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ │ │ │ len: 5, cap: 8 │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ │
│ Growth is geometric. Number of reallocations is O(log n). │
│ Each push is amortized O(1). │
│ │
└─────────────────────────────────────────────────────────────┘
Indexing vs Get
┌─────────────────────────────────────────────────────────────┐
│ v[1] │
│ │
│ Returns the element directly. │
│ Panics if the index is out of bounds. │
│ Use when the index is known to be valid. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ v.get(1) │
│ │
│ Returns Option<&T>. │
│ Some(&element) if valid, None if out of bounds. │
│ Use when the index might be invalid. │
│ │
└─────────────────────────────────────────────────────────────┘
Iteration Ownership
┌─────────────────────────────────────────────────────────────┐
│ for x in &v → x is &T │
│ Vector borrowed. Not consumed. │
│ Elements cannot be modified. │
│ │
│ for x in &mut v → x is &mut T │
│ Vector borrowed mutably. Not consumed. │
│ Elements can be modified. │
│ │
│ for x in v → x is T │
│ Vector consumed. Elements moved. │
│ Vector cannot be used afterward. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Type | Vec<T> |
| Storage | Contiguous heap allocation |
| Growth | Geometric, amortized O(1) push |
| Index | v[i] panics if out of bounds |
| Safe access | v.get(i) returns Option<&T> |
| Push | v.push(x) |
| Pop | v.pop() returns Option<T> |
| Insert | v.insert(i, x) O(n) |
| Remove | v.remove(i) O(n) |
| Fast remove | v.swap_remove(i) O(1) |
| Capacity | v.capacity() |
| Reserve | v.reserve(n) |
| Shrink | v.shrink_to_fit() |
| Iterate | iter, iter_mut, into_iter |
| Coercion | &Vec<T> to &[T] |
Key takeaways:
- A
Vec<T>is a growable array with contiguous storage. It tracks a pointer, a length, and a capacity. The length is the number of elements, and the capacity is the number of elements that fit before reallocation. - Indexing panics;
getdoes not. Usev[i]when the index is known to be valid. Usev.get(i)when it might be out of bounds. Thegetmethod returnsOption<&T>. - Push is amortized O(1) because of geometric growth. The vector allocates more capacity than it needs. When the length exceeds the capacity, it doubles (or grows by a similar factor), copies the elements, and frees the old block. The number of reallocations is logarithmic.
- Insert and remove at arbitrary positions are O(n). They shift the elements after the position. Use
swap_removewhen order does not matter, or useVecDequewhen frequent insertion and removal at both ends is needed. - Iteration has three forms with different ownership.
iterborrows,iter_mutborrows mutably, andinto_iterconsumes. Thefor x in &vform isiter,for x in &mut visiter_mut, andfor x in visinto_iter. - A
&Vec<T>coerces to&[T]. Functions should accept&[T]rather than&Vec<T>, because the slice is more general and does not constrain the caller. - Use
with_capacitywhen the size is known. It avoids the reallocations that occur during growth. Usereserveto add capacity andshrink_to_fitto release unused memory.
Remember: Vec<T> is the default collection in Rust. It owns its elements, stores them contiguously, and grows geometrically. The distinction between length and capacity explains the performance characteristics: push is fast because it usually does not reallocate, and reallocation is rare because the capacity grows geometrically. Use indexing when the index is valid, get when it might not be. Use iter to borrow, iter_mut to modify, and into_iter to consume. Use with_capacity to pre-allocate, and swap_remove when order is not needed. The vector is simple, but the details matter, and understanding them is understanding how Rust manages dynamic collections.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!