One of the things I had to unlearn coming into Rust was treating every “list of things” the same way. In Python, I’d reach for a list without thinking twice. In Rust, I actually have to decide: is this fixed-size or growable? Is every element the same type or different types? Does this live on the stack or the heap? Those questions map directly onto Rust’s three core sequence types — arrays, vectors, and tuples — and getting comfortable with the distinction is one of the more useful mental shifts I made early on. This guide covers all three in depth, including how they behave in memory and where I actually use each one.
Arrays: Fixed-Size, Stack-Allocated
A Rust array has a fixed length that’s part of its type, known at compile time. [i32; 5] and [i32; 10] are literally different types.
fn main() {
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // an array of ten zeros
println!("{:?}", numbers);
println!("{:?}", zeros);
println!("Length: {}", numbers.len());
}
Because the size is known at compile time, arrays are stored entirely on the stack (assuming the element type is also stack-allocated), which makes them extremely fast to create and access — no heap allocation, no pointer indirection.
fn sum(arr: [i32; 5]) -> i32 {
arr.iter().sum()
}
fn main() {
let nums = [10, 20, 30, 40, 50];
println!("Sum: {}", sum(nums));
}
Output:
Sum: 150
Array Bounds Checking
Rust checks array bounds at runtime for indexing operations, and panics rather than reading out-of-bounds memory:
fn main() {
let arr = [1, 2, 3];
let index = 5;
println!("{}", arr[index]); // panics: index out of bounds
}
This is a memory-safety guarantee, not an oversight — in C, this same code would silently read whatever garbage happens to sit past the array, which is a classic source of security vulnerabilities. If I want to handle out-of-bounds access gracefully instead of panicking, I use .get(), which returns an Option:
fn main() {
let arr = [1, 2, 3];
match arr.get(5) {
Some(value) => println!("Value: {value}"),
None => println!("Index out of bounds"),
}
}
Arrays are best when I know exactly how many elements I need ahead of time and that number won’t change — things like a fixed lookup table, RGB color values [u8; 3], or a chess board [[Option<Piece>; 8]; 8].
Vectors: Growable, Heap-Allocated
Vec<T> is what I reach for the vast majority of the time when I need a list. Unlike arrays, vectors can grow and shrink at runtime, and their contents live on the heap.
fn main() {
let mut scores: Vec<i32> = Vec::new();
scores.push(90);
scores.push(85);
scores.push(77);
println!("{:?}", scores);
}
The vec! macro is a more convenient way to create a vector with initial values:
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
println!("{:?}", fruits);
}
How Vectors Grow Internally
Internally, a Vec<T> is a struct holding three things: a pointer to heap-allocated memory, a length (how many elements are currently stored), and a capacity (how much memory is actually allocated). When you push past capacity, Rust allocates a new, larger buffer (typically doubling capacity), copies the existing elements over, and frees the old buffer.
fn main() {
let mut v: Vec<i32> = Vec::new();
for i in 0..5 {
v.push(i);
println!("len: {}, capacity: {}", v.len(), v.capacity());
}
}
This growth strategy is why pushing to a Vec is described as “amortized O(1)” — most pushes are cheap, but occasionally one triggers a reallocation. If I know roughly how many elements I’ll need ahead of time, I use Vec::with_capacity(n) to avoid unnecessary reallocations:
fn main() {
let mut names: Vec<String> = Vec::with_capacity(1000);
// pushing up to 1000 items won't trigger any reallocation
names.push(String::from("Ahmad"));
}
Common Vector Operations
fn main() {
let mut nums = vec![5, 3, 8, 1, 9];
nums.sort();
println!("{:?}", nums); // [1, 3, 5, 8, 9]
nums.reverse();
println!("{:?}", nums); // [9, 8, 5, 3, 1]
let doubled: Vec<i32> = nums.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
let evens: Vec<&i32> = nums.iter().filter(|&&n| n % 2 == 0).collect();
println!("{:?}", evens);
if let Some(max) = nums.iter().max() {
println!("Max: {max}");
}
}
Ownership and Borrowing with Vectors
This is where Rust’s ownership rules show up concretely. You can’t mutate a vector while holding an immutable reference into it:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // compile error: cannot borrow `v` as mutable while borrowed as immutable
println!("{first}");
}
This looks annoying at first, but it’s preventing a genuine bug: pushing to a Vec can trigger a reallocation that moves all the data to a new memory address, which would leave first pointing at freed memory — a classic use-after-free bug in other languages. Rust catches this at compile time instead of letting it become a runtime crash or, worse, a silent memory corruption bug.
Tuples: Fixed-Size, Mixed-Type Groupings
Tuples group together a fixed number of values that can be of different types. Unlike arrays and vectors, tuple elements aren’t required to share a type.
fn main() {
let person: (String, u8, bool) = (String::from("Ahmad"), 22, true);
println!("{} is {} years old. Active: {}", person.0, person.1, person.2);
}
Tuples are great for returning multiple values from a function without defining a whole new struct:
fn min_max(numbers: &[i32]) -> (i32, i32) {
let mut min = numbers[0];
let mut max = numbers[0];
for &n in numbers {
if n < min { min = n; }
if n > max { max = n; }
}
(min, max)
}
fn main() {
let data = [4, 8, 1, 9, 3];
let (smallest, largest) = min_max(&data);
println!("Min: {smallest}, Max: {largest}");
}
Output:
Min: 1, Max: 9
Destructuring Tuples
Destructuring is idiomatic Rust and shows up everywhere, from function returns to loop variables:
fn main() {
let point = (3, 7);
let (x, y) = point;
println!("x = {x}, y = {y}");
let pairs = vec![(1, "one"), (2, "two"), (3, "three")];
for (number, word) in &pairs {
println!("{number}: {word}");
}
}
The Unit Type
An empty tuple () is called the “unit type” in Rust, and it’s the default return type of functions that don’t return anything meaningful — similar to void in C, but it’s an actual, real type you can use in generics.
fn log_message(msg: &str) -> () {
println!("LOG: {msg}");
}
Choosing Between Arrays, Vectors, and Tuples
Here’s the mental model I use:
| Type | Size | Element Types | Storage | Best For |
|---|---|---|---|---|
Array [T; N] | Fixed, known at compile time | Same type | Stack | Fixed-size collections known ahead of time |
Vector Vec<T> | Dynamic, grows/shrinks | Same type | Heap | Most general-purpose lists |
Tuple (T1, T2, ...) | Fixed, small | Mixed types | Stack (if fields are) | Grouping a few heterogeneous values, function returns |
Real-World Applications
I’ve used these three in combination constantly:
- Vectors for parsing lines from a file into a growable list of records.
- Arrays for fixed-format binary protocol headers, like a 4-byte magic number
[u8; 4]. - Tuples for returning
(status_code, body)pairs from a lightweight HTTP handling function, or as map keys when combining two fields, like(user_id, session_id).
A common real pattern is a Vec of tuples or a Vec of structs when I have many records — and once a tuple starts accumulating more than three or four fields, I convert it to a proper named struct for readability, since person.1 doesn’t self-document the way person.age does.
Best Practices I Follow
- Default to
Vec<T>unless you specifically need a fixed compile-time size — arrays are a deliberate optimization, not the default choice. - Use
Vec::with_capacity()when the final size is roughly known ahead of time to avoid repeated reallocation. - Prefer named structs over tuples once you’re tracking more than two or three related values.
- Use slices (
&[T]) as function parameters instead of&Vec<T>when you just need read access — it’s more flexible since it also accepts arrays and array slices.
Common Mistakes and Debugging Tips
A mistake I made constantly early on was fighting the borrow checker over vectors — trying to read and mutate at the same time. The fix is almost always to restructure the code so borrows don’t overlap: collect indices or values you need first, then mutate afterward.
Another one: assuming Vec<T> and [T; N] are interchangeable. If a function signature expects [i32; 5] and you pass a Vec<i32>, you’ll get a type mismatch — you need .try_into() or to just accept a slice &[i32] instead, which works for both.
If you’re seeing “index out of bounds” panics in production, switch the offending indexing operation to .get() and handle the None case explicitly rather than trusting that the index will always be valid.
Frequently Asked Questions
Can I resize an array in Rust? No — arrays have a fixed size baked into their type. If you need resizing, use a Vec<T> instead.
Are tuples and structs interchangeable? Functionally similar, but tuples are positional (.0, .1) while structs have named fields. Tuples are fine for small, obvious groupings; structs are better once meaning matters.
Is a Vec<T> slower than an array? There’s a small overhead from heap allocation and indirection, but for most application-level code the difference is negligible. Arrays matter more in tight, performance-critical loops or embedded contexts without a heap.
How do I convert a Vec<T> to an array? Use .try_into() if you know the exact length at that point in the code — it returns a Result since the conversion can fail if lengths don’t match.
Summary
Arrays, vectors, and tuples all represent “groups of values” in Rust, but each makes a different tradeoff between size flexibility, type uniformity, and memory location. Arrays are fixed and fast; vectors are flexible and heap-backed; tuples group a handful of differently typed values together, often for quick function returns. Learning to pick the right one — instead of defaulting to whatever felt familiar from another language — made my Rust code both faster and easier to reason about.
References
- The Rust Programming Language Book — Chapter on Common Collections (doc.rust-lang.org/book)
- The Rust Programming Language Book — Chapter on Data Types (doc.rust-lang.org/book)
- Official Rust Standard Library documentation for
Vec<T>, arrays, and tuples (doc.rust-lang.org/std) - The Cargo Book (doc.rust-lang.org/cargo)
