Tuples & Arrays
Reviewed & published by Brayan K
Tuples and arrays are Rust's two fixed-size compound types — a tuple bundles a set number of values of different types, while an array holds a fixed-length run of values that all share one type.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll destructure tuples, index and iterate arrays, understand bounds safety, and see how arrays relate to slices and the growable Vec.
What You'll Learn in This Lesson
- Building tuples of mixed types and reading .0/.1 fields
- Destructuring tuples into named variables
- Fixed arrays [T; N] and the [value; count] shorthand
- Indexing, len(), and bounds safety (panic vs .get())
- Iterating elements directly with a for loop
- How arrays relate to slices and the growable Vec
1️⃣ Tuples: Grouping Mixed Values
A tuple packs a fixed number of values, each possibly a different type, into one value. You read its fields by position — .0, .1, .2 — or, more idiomatically, you destructure it into named variables.
fn main() {
// A tuple groups a FIXED number of values of POSSIBLY DIFFERENT types.
let person: (&str, i32, f64) = ("Ada", 36, 1.65);
// Access fields by position with .0, .1, .2 ...
println!("name = {}", person.0);
println!("age = {}", person.1);
println!("height = {}", person.2);
// DESTRUCTURE a tuple into named variables in one line.
let (name, age, height) = person;
println!("{} is {} and {}m tall", name, age, height);
// The empty tuple () is the "unit" type — Rust's "no meaningful value".
let nothing: () = ();
println!("unit prints as: {:?}", nothing);
}The empty tuple () is special: it's the unit type, Rust's way of saying "no meaningful value." Functions that return nothing actually return (). Tuples are perfect for returning a couple of values from a function without defining a whole struct.
2️⃣ Arrays: Fixed-Length, Same Type
An array's type is [T; N] — element type T and a compile-time length N that never changes. You index from zero, ask its length with .len(), and iterate elements directly. The [value; count] shorthand fills an array with a repeated value.
fn main() {
// An array is a FIXED-LENGTH sequence of the SAME type: [T; N].
let scores: [i32; 4] = [90, 85, 70, 100];
// Index from 0. len() gives the length.
println!("first = {}", scores[0]);
println!("last = {}", scores[3]);
println!("len = {}", scores.len());
// [value; count] fills an array with a repeated value.
let zeros = [0; 5];
println!("zeros = {:?}", zeros);
// Iterate directly over the elements.
let mut total = 0;
for s in scores {
total += s;
}
println!("total = {}", total);
}3️⃣ Slices and the Growable Vec
A slice (&[T]) borrows a contiguous part of an array or Vec using a range like &data[1..4] (the end index is exclusive). When you need a collection that can grow at runtime, reach for Vec<T> — a heap-backed, resizable array you build with push or the vec! macro.
fn main() {
let data = [10, 20, 30, 40, 50];
// A SLICE borrows a contiguous part of an array: &data[start..end] (end exclusive).
let middle = &data[1..4];
println!("middle = {:?}", middle);
println!("middle len = {}", middle.len());
// A Vec is a GROWABLE array allocated on the heap.
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
println!("vec = {:?}", v);
println!("vec len = {}", v.len());
// The vec! macro builds one directly.
let v2 = vec![7, 8, 9];
println!("v2 sum = {}", v2.iter().sum::<i32>());
}Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — fill in the blanks, then run it.
fn main() {
let point = (3, 7);
// 1) Destructure the tuple into x and y.
let (___, ___) = point;
println!("x = {}, y = {}", x, y);
// 2) Access the SECOND field by position.
println!("point.1 = {}", point.___);
let nums = [4, 8, 15, 16];
// 3) Print how many elements the array has.
println!("len = {}", nums.___());
}
// ✅ Expected output:
// x = 3, y = 7
// point.1 = 7
// len = 4Mini-Challenge: Temperature Log
Loop over an array of tuples, destructure each one, and track an average and a maximum. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: temperature log.
//
// You have an array of (hour, temperature) tuples:
// [(8, 21.5), (12, 25.0), (18, 19.5)]
// 1. Sum the temperatures and compute the average (sum / count as f64).
// 2. Track the warmest reading by destructuring each tuple in the loop
// and comparing the temperature field.
// 3. Print the average and the warmest reading.
//
// ✅ Example output:
// average = 22
// warmest = 25C at 12h
fn main() {
let readings = [(8, 21.5), (12, 25.0), (18, 19.5)];
// your code here
}Common Errors (and the fix)
- ❌ "index out of bounds" panic — You indexed past the last element. ✅ Check .len(), or use .get(i) which returns an Option.
- ❌ "mismatched types" filling an array — Every element of [T; N] must be the same type. ✅ Use a tuple or a struct when the items differ.
- ❌ Trying to push to an array — Arrays are fixed-length and can't grow. ✅ Use a Vec<T> if the size changes at runtime.
- ❌ tuple[0] doesn't compile — Tuples aren't indexed with brackets. ✅ Use field access tuple.0 or destructure.
Pro Tips
- 💡 Return a tuple to hand back two or three values from a function without defining a struct.
- 💡 Print with {:?} to debug-display whole arrays, tuples, and Vecs at once.
- 💡 Accept slices in function parameters (&[T]) so one function serves arrays and Vecs alike.
📋 Quick Reference — Tuples & Arrays
| Task | Syntax |
|---|---|
| Tuple | let t = (1, "a", 2.0); |
| Tuple field | t.0 t.1 |
| Destructure | let (a, b, c) = t; |
| Array | let a: [i32; 3] = [1,2,3]; |
| Fill / slice | [0; 5] &a[1..3] |
| Vec | vec![1, 2, 3] |
🎉 Lesson Complete!
- ✅ Tuples group mixed types; read with .0/.1 or destructure
- ✅ Arrays [T; N] are fixed-length, same-type, zero-indexed
- ✅ Out-of-bounds indexing panics; .get() returns an Option safely
- ✅ Slices (&[T]) borrow a range; Vec<T> grows at runtime
Practice quiz
How do you access the second field of a tuple t?
- t[1]
- t->1
- t.1
- t.second
Answer: t.1. Tuples are accessed by position with dot syntax: t.0, t.1, t.2.
What is the type signature of an array of three i32 values?
- [i32; 3]
- [3; i32]
- Array<i32, 3>
- Vec<i32>
Answer: [i32; 3]. An array type is written [T; N] with element type then length.
Can a single tuple hold values of different types?
- No, all elements must match
- Only if they are numbers
- Only two values
- Yes, a tuple groups mixed types
Answer: Yes, a tuple groups mixed types. A tuple like ("Ada", 36, 1.65) bundles different types together.
Must all elements of an array share one type?
- No, any types allowed
- Yes, arrays are same-type
- Only the first two
- Only references
Answer: Yes, arrays are same-type. An array [T; N] holds a fixed run of values that all have type T.
What does the [0; 5] expression create?
- An array of five zeros
- A range from 0 to 5
- A tuple of two values
- An empty array
Answer: An array of five zeros. [value; count] fills an array with a repeated value, here five 0s.
Is an array's length fixed at compile time?
- No, it can grow
- Only with push
- Yes, it never changes
- Only on the heap
Answer: Yes, it never changes. Arrays are fixed-length; use a Vec when the size must change at runtime.
What happens when you index an array out of bounds with [i]?
- It returns None
- The program panics
- It reads garbage memory
- It returns 0
Answer: The program panics. Rust checks bounds and panics rather than reading invalid memory.
What does scores.get(99) return when the index is out of range?
- Some(0)
- A panic
- -1
- None
Answer: None. .get(i) returns an Option: None when the index is invalid, Some(&value) otherwise.
What does &data[1..4] produce on an array?
- A copy of the array
- A slice borrowing elements 1, 2, 3
- Elements 1 through 4 inclusive
- A tuple
Answer: A slice borrowing elements 1, 2, 3. A slice borrows a contiguous range; the end index 4 is exclusive.
How do you destructure a tuple t into a and b?
- let [a, b] = t;
- let a, b = t;
- let (a, b) = t;
- let {a, b} = t;
Answer: let (a, b) = t;. Tuple destructuring uses parentheses: let (a, b) = t;.
Continue this course
- Previous: Operators & Expressions
- Next: String Formatting (format!, println!) — {} vs {:?}, width/precision/alignment, and named & inline args
- Quick reference: Rust cheat sheet › Collections
Frequently asked questions
What's the difference between a tuple and an array?
A tuple groups a fixed number of values that can each be a different type — for example ("Ada", 36, 1.65) holds a string, an integer, and a float together. You access tuple fields by position with .0, .1, .2 and so on, or by destructuring. An array, written [T; N], is a fixed-length sequence where every element has the same type T, and you access elements by an index in square brackets. Use a tuple to bundle a few related-but-different values; use an array for a fixed run of same-typed items.
When should I use an array versus a Vec?
Reach for a plain array, [T; N], when the length is known at compile time and never changes — like the four suits in a deck or a fixed lookup table. Arrays live on the stack and carry no allocation cost. Use a Vec<T> when the number of elements can grow or shrink at runtime, because a Vec is heap-allocated and supports push, pop, and resizing. A handy bridge between them is the slice (&[T]), which borrows a contiguous run of elements from either an array or a Vec, so functions usually take a slice to accept both.
What happens if I index an array out of bounds?
Rust guarantees memory safety, so an out-of-bounds index does not read garbage memory the way it might in C. For a constant index the compiler can reject it at build time, and for a runtime index that's too large the program panics with a clear 'index out of bounds' message instead of silently corrupting data. If you want to handle a possibly-missing element gracefully, use the .get(i) method, which returns an Option: Some(value) when the index is valid and None when it isn't.
Why does iterating over an array with `for x in arr` work without indexes?
Rust's for loop works on iterators, and arrays, slices, and Vecs can all produce one. Writing for x in scores hands you each element in turn, which is clearer and safer than manually indexing because there's no chance of an off-by-one or out-of-bounds mistake. If you also need the position, wrap it with .iter().enumerate() to get (index, value) pairs. Indexing by hand is mostly reserved for cases where you genuinely need arbitrary, non-sequential access.