| |

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

OperationSyntaxExample
Declare(T1, T2)(i32, f64)
Create(v1, v2)(3, 5)
Access.indexpoint.0
Destructurelet (a, b) = tlet (x, y) = point
Ignore_let (x, _, z) = t
Rest..let (first, ..) = t
Unit()fn f() -> ()

Array Operations

OperationSyntaxExample
Declare[T; N][i32; 5]
Create[v1, v2, ...][1, 2, 3]
Repeat[value; N][0; 10]
Accessarr[i]arr[0]
Length.len()arr.len()
Iteratefor x in arrby value
Iteratefor x in &arrby reference
Mutatefor x in &mut arrmutable reference

Tuple vs Array

AspectTupleArray
Element typesDifferent allowedSame only
LengthFixedFixed
Type syntax(T1, T2)[T; N]
Access.0, .1[0], [1]
IterationNot directfor x in arr
Stack allocatedYesYes

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

PitfallWhy It HappensFix
Index out of bounds panicIndex >= array lengthCheck i < arr.len() or use iterators
(42) is not a tupleMissing trailing commaUse (42,) for one-element tuple
Tuple index with variable failsIndex must be a literalUse destructuring instead
Array size mismatchLength is part of the typeMatch the declared length
Cannot iterate tuple directlyTuples are not iterableDestructure first
Mutable iteration failsMissing &mutUse 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

ItemValue
Tuple type(T1, T2, ...)
Tuple access.0, .1, etc.
Tuple destructuringlet (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 accessarr[index]
Bounds checkingRuntime panic on out-of-bounds
Array lengtharr.len()
Iterationfor 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) = point binds x and y to 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 be Copy or 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 &arr is 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!