Rust 6 🦀 Compound Data Types: Tuples and Arrays
Rust’s scalar types hold a single value. Compound types group multiple values into one. The two primitive compound types are tuples and arrays. Both have a fixed size known at compile time, and both store their elements contiguously, but they differ in a crucial way: a tuple can hold values of different types, while an array holds values of the same type. This distinction determines which one to reach for in a given situation. A tuple is for grouping a fixed number of related but differently-typed values, like a coordinate pair or a function’s multiple return values. An array is for a fixed-size collection of same-typed elements, like a lookup table or a buffer of bytes.
Neither tuple nor array can grow or shrink. That limitation is deliberate: their size is part of their type, which means the compiler knows exactly how much memory they occupy and can check every access. When you need a collection that can grow, you use Vec<T> or another standard library type. But for fixed-size data, tuples and arrays are efficient and precise.
This chapter covers tuple syntax, tuple destructuring, tuple indexing, the unit type, array syntax, array initialization, array indexing, array bounds checking, and the operations that work on both.
Key point: Tuples group values of different types; arrays hold values of the same type. Both have fixed sizes known at compile time. Access tuple elements with .0, .1, etc.; access array elements with [index]. Array indexing is bounds-checked at runtime and panics on out-of-bounds access.
Why tuples and arrays exist
The grouping problem. Functions sometimes need to return more than one value. Without a compound type, returning two values would require allocating a struct or using out-parameters. A tuple provides a lightweight way to group a fixed number of values, potentially of different types, without declaring a struct. (i32, f64) is a valid type that holds an integer and a float.
The fixed-size collection problem. Some data is naturally fixed-size: the three components of an RGB color, the twelve months of a year, the bytes of a hash. An array expresses this exactly. The type [u8; 32] is a 32-byte array, and its size is part of its type. The compiler can check that operations preserve the size, and no heap allocation is required.
The stack allocation problem. Both tuples and arrays are stack-allocated when they are local variables. They do not require heap allocation, which makes them fast to create and destroy. For small, fixed-size data, this is the most efficient representation available.
The pattern matching problem. Rust’s pattern matching works naturally with tuples and arrays. Destructuring let (x, y) = point; extracts the fields of a tuple in one statement. Matching [first, .., last] extracts the first and last elements of an array. This integration makes both types ergonomic to use.
The bounds checking problem. Accessing an array element out of bounds is a classic source of security vulnerabilities in C and C++. Rust checks array indices at runtime and panics on out-of-bounds access, converting a potential memory corruption into a controlled failure. The compiler can also eliminate bounds checks when it can prove the index is valid, so the safety does not always cost performance.
a. Tuple types and values
A tuple is a fixed-length sequence of values of potentially different types. The type is written as a comma-separated list of component types in parentheses:
let point: (i32, i32) = (3, 5);
let mixed: (i32, f64, char) = (42, 3.14, 'x');
The values are written in the same order as the types. A tuple with one element requires a trailing comma to distinguish it from a parenthesized expression:
let single: (i32,) = (42,); // one-element tuple
let not_a_tuple = (42); // just 42, type is i32
The trailing comma matters. (42) is an integer in parentheses; (42,) is a tuple containing one integer.
Tuples are most useful for grouping a small number of related values where a struct would be overkill. Function return types are a common case:
fn min_max(values: &[i32]) -> (i32, i32) {
let mut min = values[0];
let mut max = values[0];
for &v in values {
if v < min { min = v; }
if v > max { max = v; }
}
(min, max)
}
let (low, high) = min_max(&[3, 1, 4, 1, 5]);
The function returns a tuple, and the caller destructures it into two variables.
b. Tuple destructuring
Destructuring extracts the components of a tuple into separate variables. The pattern on the left of let mirrors the structure of the tuple on the right:
let point = (3, 5);
let (x, y) = point;
println!("x = {}, y = {}", x, y);
Destructuring can be selective using _ for components you do not need:
let (x, _, z) = (1, 2, 3);
println!("x = {}, z = {}", x, z); // x = 1, z = 3
The .. pattern ignores the rest of the tuple:
let (first, ..) = (1, 2, 3, 4);
println!("{}", first); // 1
Destructuring also works in function parameters:
fn distance((x1, y1): (i32, i32), (x2, y2): (i32, i32)) -> f64 {
let dx = (x2 - x1) as f64;
let dy = (y2 - y1) as f64;
(dx * dx + dy * dy).sqrt()
}
This is less common than destructuring in the function body but is valid and sometimes convenient.
c. Tuple indexing
Individual tuple elements are accessed with dot notation and a zero-based index:
let point = (3, 5);
println!("x = {}", point.0); // 3
println!("y = {}", point.1); // 5
The index must be a literal integer, not a variable. point.0 works; point.i where i is a variable does not. This is because each field of a tuple has a distinct type, and the compiler needs to know the type at compile time.
let tuple = (42, "hello", 3.14);
let n = tuple.0; // i32
let s = tuple.1; // &str
let f = tuple.2; // f64
Tuple indexing is only available for tuples up to 12 elements, because the standard library defines Index implementations for those arities. Beyond 12 elements, a struct is a better choice.
d. The unit type
A tuple with no elements, written (), is called the unit type. It has exactly one value, also written (). It is the type of expressions and functions that return nothing meaningful:
fn print_hello() {
println!("Hello");
// implicitly returns ()
}
fn print_hello_explicit() -> () {
println!("Hello");
}
The unit type appears in generic contexts as a placeholder, in Result<(), Error> for functions that can fail but return no value, and as the return type of statements that produce no value.
e. Array types and values
An array is a fixed-size collection of values of the same type. The type is written [T; N], where T is the element type and N is the length:
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let bytes: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
The length is part of the type. [i32; 5] and [i32; 3] are different types, and the compiler will reject an assignment between them.
An array can be initialized with a repeated value:
let zeros = [0; 10]; // ten zeros, type [i32; 10]
let spaces = [' '; 100]; // one hundred spaces, type [char; 100]
The value before the semicolon is copied to each element. The value must be Copy or the expression must be a constant that can be evaluated repeatedly.
Array length is available as a constant via the len() method:
let arr = [1, 2, 3, 4, 5];
println!("{}", arr.len()); // 5
Because the length is known at compile time, len() returns a constant, and the compiler can use it in type-level contexts.
f. Array indexing and bounds checking
Array elements are accessed with square brackets and a zero-based index:
let arr = [10, 20, 30, 40, 50];
println!("{}", arr[0]); // 10
println!("{}", arr[4]); // 50
Indexing is bounds-checked at runtime. Accessing an index that is out of bounds panics:
let arr = [10, 20, 30];
let index = 5;
// println!("{}", arr[index]); // panics: index out of bounds
The panic is a controlled failure. It does not corrupt memory, and it names the offending index and the array length in the error message. This is fundamentally different from C, where an out-of-bounds access is undefined behavior.
When the compiler can prove the index is in bounds, it eliminates the runtime check. For example, iterating with a for loop over an array does not perform bounds checks:
let arr = [10, 20, 30];
for value in arr {
println!("{}", value); // no bounds check needed
}
Iterating by index does perform checks, because the compiler cannot prove the index is valid:
for i in 0..arr.len() {
println!("{}", arr[i]); // bounds check
}
The iterator form is both safer and faster. Prefer it when possible.
g. Iterating and operating on arrays
Arrays support iteration by value, by reference, and by mutable reference:
let mut arr = [1, 2, 3, 4, 5];
for value in arr { // by value (copies)
println!("{}", value);
}
for value in &arr { // by reference
println!("{}", value);
}
for value in &mut arr { // by mutable reference
*value *= 2;
}
Array methods include len(), first(), last(), iter(), contains(), starts_with(), and ends_with(). Slices — &[T] — are views into arrays and other contiguous collections, and many array operations are actually slice operations accessed through deref coercion.
A common pattern is iterating with an index and a value using enumerate():
let names = ["Alice", "Bob", "Carol"];
for (i, name) in names.iter().enumerate() {
println!("{}: {}", i, name);
}
Complete Example Session
// ============================================
// PART 1: TUPLE DECLARATION AND ACCESS
// ============================================
fn main() {
let point: (i32, i32) = (3, 5);
println!("x = {}, y = {}", point.0, point.1);
}
// ============================================
// PART 2: TUPLE DESTRUCTURING
// ============================================
fn main() {
let point = (3, 5);
let (x, y) = point;
println!("x = {}, y = {}", x, y);
}
// ============================================
// PART 3: MIXED-TYPE TUPLE
// ============================================
fn main() {
let record: (u32, &str, f64) = (42, "hello", 3.14);
println!("{} {} {}", record.0, record.1, record.2);
}
// ============================================
// PART 4: TUPLE AS RETURN TYPE
// ============================================
fn min_max(values: &[i32]) -> (i32, i32) {
let mut min = values[0];
let mut max = values[0];
for &v in values {
if v < min { min = v; }
if v > max { max = v; }
}
(min, max)
}
fn main() {
let (low, high) = min_max(&[3, 1, 4, 1, 5]);
println!("low = {}, high = {}", low, high);
}
// ============================================
// PART 5: SELECTIVE DESTRUCTURING
// ============================================
fn main() {
let (x, _, z) = (1, 2, 3);
println!("x = {}, z = {}", x, z);
let (first, ..) = (10, 20, 30, 40);
println!("first = {}", first);
}
// ============================================
// PART 6: UNIT TYPE
// ============================================
fn log(message: &str) {
println!("[LOG] {}", message);
// returns ()
}
fn main() {
let result: () = log("hello");
println!("{:?}", result); // ()
}
// ============================================
// PART 7: ARRAY DECLARATION AND ACCESS
// ============================================
fn main() {
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
println!("{}", numbers[0]);
println!("{}", numbers[4]);
println!("length = {}", numbers.len());
}
// ============================================
// PART 8: ARRAY INITIALIZATION
// ============================================
fn main() {
let zeros = [0; 10];
let spaces = [' '; 5];
println!("{:?}", zeros);
println!("{:?}", spaces);
}
// ============================================
// PART 9: ARRAY BOUNDS CHECKING
// ============================================
fn main() {
let arr = [10, 20, 30];
let index = 1;
println!("{}", arr[index]); // 20
// Uncomment to see the panic:
// let bad = 5;
// println!("{}", arr[bad]);
}
// ============================================
// PART 10: ITERATING AND MUTATING ARRAYS
// ============================================
fn main() {
let mut scores = [85, 92, 78, 95];
for score in &mut scores {
*score += 5;
}
for (i, score) in scores.iter().enumerate() {
println!("Score {}: {}", i, score);
}
}
These ten parts cover tuple declaration, destructuring, mixed-type tuples, tuples as return values, selective destructuring, the unit type, array declaration, array initialization, bounds checking, and iteration with mutation.
Quick Reference
Tuple Operations
| Operation | Syntax | Example |
|---|---|---|
| Declare | (T1, T2) | (i32, f64) |
| Create | (v1, v2) | (3, 5) |
| Access | .index | point.0 |
| Destructure | let (a, b) = t | let (x, y) = point |
| Ignore | _ | let (x, _, z) = t |
| Rest | .. | let (first, ..) = t |
| Unit | () | fn f() -> () |
Array Operations
| Operation | Syntax | Example |
|---|---|---|
| Declare | [T; N] | [i32; 5] |
| Create | [v1, v2, ...] | [1, 2, 3] |
| Repeat | [value; N] | [0; 10] |
| Access | arr[i] | arr[0] |
| Length | .len() | arr.len() |
| Iterate | for x in arr | by value |
| Iterate | for x in &arr | by reference |
| Mutate | for x in &mut arr | mutable reference |
Tuple vs Array
| Aspect | Tuple | Array |
|---|---|---|
| Element types | Different allowed | Same only |
| Length | Fixed | Fixed |
| Type syntax | (T1, T2) | [T; N] |
| Access | .0, .1 | [0], [1] |
| Iteration | Not direct | for x in arr |
| Stack allocated | Yes | Yes |
Best Practices
✅ Do This:
let (x, y) = point; // Destructure for clarity
let (first, ..) = tuple; // Use .. for remaining
let arr = [0; 1024]; // Repeat syntax for large arrays
for value in &arr { } // Iterate by reference
println!("{}", arr[0]); // Direct index when known safe
let (min, max) = min_max(&values); // Tuple return for multiple values
❌ Don’t Do This:
let x = point.0; let y = point.1; // ❌ Destructure instead
let arr = [0, 0, 0, 0, 0, 0, 0, 0]; // ❌ Use [0; 8]
let x = arr[i]; // ❌ Without checking i < len
let single = (42); // ❌ Not a tuple; use (42,)
let huge = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); // ❌ >12 elements
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Index out of bounds panic | Index >= array length | Check i < arr.len() or use iterators |
(42) is not a tuple | Missing trailing comma | Use (42,) for one-element tuple |
| Tuple index with variable fails | Index must be a literal | Use destructuring instead |
| Array size mismatch | Length is part of the type | Match the declared length |
| Cannot iterate tuple directly | Tuples are not iterable | Destructure first |
| Mutable iteration fails | Missing &mut | Use for x in &mut arr |
Real-World Examples
1. Coordinate Pair
let origin: (i32, i32) = (0, 0);
let (x, y) = origin;
2. Multiple Return Values
fn divmod(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (quotient, remainder) = divmod(17, 5);
3. Swap Two Variables
let (mut a, mut b) = (1, 2);
(a, b) = (b, a);
4. Fixed-Size Buffer
let mut buffer: [u8; 1024] = [0; 1024];
buffer[0] = 0xFF;
5. Lookup Table
let hex_digits = ['0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f'];
6. Array of Structs
struct Point { x: i32, y: i32 }
let points: [Point; 3] = [
Point { x: 0, y: 0 },
Point { x: 1, y: 0 },
Point { x: 0, y: 1 },
];
7. Iterating with Index
let names = ["Alice", "Bob", "Carol"];
for (i, name) in names.iter().enumerate() {
println!("{}: {}", i, name);
}
8. Summing an Array
let values = [1, 2, 3, 4, 5];
let sum: i32 = values.iter().sum();
9. Finding an Element
let arr = [10, 20, 30];
if arr.contains(&20) {
println!("found");
}
10. Slice from Array
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // [2, 3, 4]
Visual
Tuple vs Array
┌──────────────────────────────────────────────────────────────┐
│ TUPLE ARRAY │
│ │
│ (i32, f64, char) [i32; 5] │
│ ┌──────┬──────┬──────┐ ┌────┬────┬────┬────┬────┐ │
│ │ i32 │ f64 │ char │ │ i32│ i32│ i32│ i32│ i32│ │
│ └──────┴──────┴──────┘ └────┴────┴────┴────┴────┘ │
│ Different types allowed Same type only │
│ │
│ Access: point.0, point.1 Access: arr[0], arr[1] │
│ Destructure: let (a, b) = t Iterate: for x in arr │
└──────────────────────────────────────────────────────────────┘
Tuple Destructuring
┌──────────────────────────────────────────────────────────────┐
│ let point = (3, 5); │
│ │
│ let (x, y) = point; │
│ │ │ │
│ │ └── y = 5 │
│ └── x = 3 │
│ │
│ let (x, _, z) = (1, 2, 3); │
│ │ │ │
│ │ └── _ ignores the middle value │
│ └── x = 1 │
│ │
│ let (first, ..) = (1, 2, 3, 4); │
│ │ │ │
│ │ └── .. ignores the rest │
│ └── first = 1 │
└──────────────────────────────────────────────────────────────┘
Array Bounds Checking
┌──────────────────────────────────────────────────────────────┐
│ let arr = [10, 20, 30]; │
│ │
│ arr[0] → 10 ✅ in bounds │
│ arr[2] → 30 ✅ in bounds │
│ arr[3] → panic: index out of bounds │
│ │
│ ┌────┬────┬────┐ │
│ │ 10 │ 20 │ 30 │ length = 3 │
│ └────┴────┴────┘ │
│ [0] [1] [2] [3] is out of bounds │
│ │
│ The runtime check prevents memory corruption. │
│ Iterators can eliminate the check. │
└──────────────────────────────────────────────────────────────┘
Array Iteration
┌──────────────────────────────────────────────────────────────┐
│ let mut arr = [1, 2, 3]; │
│ │
│ for v in arr { } → by value (copies) │
│ for v in &arr { } → by reference │
│ for v in &mut arr { } → mutable reference │
│ │
│ for (i, v) in arr.iter().enumerate() { } │
│ └── i = index, v = value │
│ │
│ Iterators are preferred over index loops: │
│ - No bounds check needed │
│ - Cannot go out of bounds │
│ - Often faster │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Tuple type | (T1, T2, ...) |
| Tuple access | .0, .1, etc. |
| Tuple destructuring | let (a, b) = tuple |
| One-element tuple | (value,) with trailing comma |
| Unit type | (), no value |
| Array type | [T; N] |
| Array literal | [v1, v2, ...] |
| Array repeat | [value; N] |
| Array access | arr[index] |
| Bounds checking | Runtime panic on out-of-bounds |
| Array length | arr.len() |
| Iteration | for x in arr, for x in &arr, for x in &mut arr |
Key takeaways:
- Tuples hold different types; arrays hold the same type. The type syntax reflects this:
(i32, f64)versus[i32; 5]. - Tuples are accessed by
.0,.1, etc. The index must be a literal integer, not a variable, because each field has a distinct type. - Destructuring extracts tuple components.
let (x, y) = pointbindsxandyto the tuple’s elements in one statement. - Arrays have fixed lengths that are part of their type.
[i32; 5]and[i32; 3]are different types, and the compiler rejects mixing them. - Arrays are initialized with a list or a repeated value.
[0; 10]creates an array of ten zeros; the value must beCopyor a constant. - Array indexing is bounds-checked. Out-of-bounds access panics rather than corrupting memory. This is a safety guarantee that C and C++ do not provide.
- Iterators avoid bounds checks. Iterating with
for x in &arris both safer and faster than indexing in a loop, because the compiler knows the iteration stays in bounds. - The unit type
()is the empty tuple. It is the type of functions that return nothing and expressions that produce no value.
Remember: Tuples and arrays are the primitive compound types in Rust. Both have fixed sizes known at compile time, and both are stack-allocated when used as local variables. The choice between them is determined by whether the elements have the same type or different types. Tuples group differently-typed values, usually a small number, and are commonly used for multiple return values and destructuring. Arrays hold a fixed number of same-typed values and are used for buffers, lookup tables, and other fixed-size collections. Both integrate with pattern matching, and arrays support iteration that the compiler can optimize by eliminating bounds checks. Understanding these two types is understanding how Rust groups data without heap allocation.
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!