Guide contents

Lesson 9 of 94

Tuples & Arrays

Rust has two built-in ways to group several values into one: tuples, which hold a fixed number of values of possibly different types, and arrays, which hold a fixed number of values of the same type.

Concept Rust
A small group of mixed values Tuple (i32, &str)
Fixed-size array let a = [0; 5]; with type [i32; 5]
Length a.len()
Bad index Compile error or panic
Assignment Arrays are values: assignment copies them

A growable list is a Vec, which you meet in the next lesson.

Part 1: Tuples

1. Creating a tuple

A tuple is written as values in parentheses. Tuples cannot be printed with {}, so use the Debug format {:?}.

Rust
fn main() {
    let point = (3, 7);
    let product = ("Keyboard", 49.99, true);
    println!("{:?}", point);
    println!("{:?}", product);
}
Output
(3, 7)
("Keyboard", 49.99, true)

2. The tuple's type

The type of a tuple lists the type of each element. You can write it out, just like any other type annotation.

Rust
fn main() {
    let employee: (u32, &str, f64) = (1042, "Grace", 8_250.0);
    println!("{:?}", employee);
}
Output
(1042, "Grace", 8250.0)

3. Accessing elements by position

Use .0, .1, .2, and so on. Positions start at zero.

Rust
fn main() {
    let employee = (1042, "Grace", 8_250.0);
    println!("id: {}", employee.0);
    println!("name: {}", employee.1);
    println!("salary: {}", employee.2);
}
Output
id: 1042
name: Grace
salary: 8250

4. Destructuring a tuple

A let can take a tuple apart into separate variables in one step. This is called destructuring.

Rust
fn main() {
    let employee = (1042, "Grace", 8_250.0);
    let (id, name, salary) = employee;
    println!("{name} (#{id}) earns {salary}");
}
Output
Grace (#1042) earns 8250

5. Ignoring parts with _

Rust
fn main() {
    let http_response = (200, "OK", "<html>...</html>");
    let (status, _, _) = http_response;
    let (_, reason, ..) = http_response; // `..` ignores all the rest
    println!("{status} {reason}");
}
Output
200 OK

6. Returning several values from a function

This is the most common use of tuples.

Rust
fn min_max(a: i32, b: i32, c: i32) -> (i32, i32) {
    let min = a.min(b).min(c);
    let max = a.max(b).max(c);
    (min, max)
}

fn main() {
    let (lowest, highest) = min_max(17, -4, 9);
    println!("min={lowest}, max={highest}");
}
Output
min=-4, max=17

7. Tuples as parameters

Rust
fn distance(from: (f64, f64), to: (f64, f64)) -> f64 {
    let dx = to.0 - from.0;
    let dy = to.1 - from.1;
    (dx * dx + dy * dy).sqrt()
}

fn main() {
    let home = (0.0, 0.0);
    let office = (3.0, 4.0);
    println!("distance: {}", distance(home, office));
}
Output
distance: 5

8. Destructuring in parameters

A parameter can itself be a pattern, which destructures the tuple as it arrives.

Rust
fn describe((width, height): (u32, u32)) -> String {
    format!("{width}x{height} ({} pixels)", width * height)
}

fn main() {
    println!("{}", describe((1920, 1080)));
}
Output
1920x1080 (2073600 pixels)

9. Swapping values

Destructuring assignment swaps two variables without a temporary variable.

Rust
fn main() {
    let mut first = "left";
    let mut second = "right";
    (first, second) = (second, first);
    println!("{first} {second}");
}
Output
right left

10. Mutable tuples

Rust
fn main() {
    let mut position = (0, 0);
    position.0 += 5;
    position.1 -= 2;
    println!("{:?}", position);
}
Output
(5, -2)

11. Comparing tuples

Tuples with comparable elements can be compared with == and <. Comparison is lexicographic: the first elements are compared first, and later elements only break ties. This is a handy way to compare versions.

Rust
fn main() {
    let installed = (1, 9, 4);
    let required = (1, 10, 0);
    println!("equal? {}", installed == required);
    println!("outdated? {}", installed < required);
    println!("{}", (2, 0) > (1, 99));
}
Output
equal? false
outdated? true
true

12. Nested tuples

Rust
fn main() {
    let route = (("Lisbon", 38.72), ("Porto", 41.15));
    let ((from, from_lat), (to, to_lat)) = route;
    println!("{from} ({from_lat}) -> {to} ({to_lat})");
    println!("{}", (route.1).0);
}
Output
Lisbon (38.72) -> Porto (41.15)
Porto

13. The empty tuple ()

The tuple with no elements, (), is the unit type from Functions. It is the value of expressions that produce "nothing".

Rust
fn main() {
    let nothing = ();
    println!("{:?}", nothing);
}
Output
()

Part 2: Arrays

14. Creating an array

An array holds a fixed number of values of the same type. Its type is written [Type; length], and the length is part of the type: [i32; 3] and [i32; 4] are different types.

Rust
fn main() {
    let scores: [i32; 5] = [88, 92, 75, 64, 99];
    let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri"];
    println!("{:?}", scores);
    println!("{:?}", weekdays);
}
Output
[88, 92, 75, 64, 99]
["Mon", "Tue", "Wed", "Thu", "Fri"]

15. Arrays with a repeated value

[value; count] creates an array filled with the same value.

Rust
fn main() {
    let zeros = [0; 8];
    let seats = [true; 4];
    println!("{:?}", zeros);
    println!("{:?}", seats);
}
Output
[0, 0, 0, 0, 0, 0, 0, 0]
[true, true, true, true]

16. Indexing and length

Indexes start at 0 and must be usize values.

Rust
fn main() {
    let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
    println!("first: {}", months[0]);
    println!("last: {}", months[months.len() - 1]);
    println!("count: {}", months.len());
}
Output
first: Jan
last: Jun
count: 6

17. An out-of-bounds index the compiler can see

If the index is known at compile time and is too large, the program does not compile.

Rust✗ does not compile
fn main() {
    let months = ["Jan", "Feb", "Mar"];
    println!("{}", months[5]);
}
Output
error: this operation will panic at runtime
 --> src/main.rs:3:20
  |
3 |     println!("{}", months[5]);
  |                    ^^^^^^^^^ index out of bounds: the length is 3 but the index is 5
  |
  = note: `#[deny(unconditional_panic)]` on by default

18. An out-of-bounds index at runtime panics

If the index is only known at runtime, a bad index panics with a clear message. It never reads random memory, the classic C bug.

Rustpanics at runtime
fn month_name(number: usize) -> &'static str {
    let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    months[number - 1]
}

fn main() {
    println!("{}", month_name(9));
    println!("{}", month_name(13));
}
Output
Sep

thread 'main' panicked at src/main.rs:4:5:
index out of bounds: the len is 12 but the index is 12

To get a value safely, months.get(i) returns an Option instead of panicking. See Option.

19. Looping over an array

A for loop visits each element in order.

Rust
fn main() {
    let prices = [4.50, 12.99, 3.25];
    let mut total = 0.0;
    for price in prices {
        total += price;
    }
    println!("total: {total:.2}");
}
Output
total: 20.74

20. Looping with the index

Two ways: loop over the index range, or use .iter().enumerate(), which yields (index, element) tuples. The second is more idiomatic and avoids any chance of an index mistake. Iterators have their own lesson.

Rust
fn main() {
    let podium = ["Ana", "Ben", "Chloe"];
    for i in 0..podium.len() {
        println!("#{} {}", i + 1, podium[i]);
    }
    for (i, name) in podium.iter().enumerate() {
        println!("place {}: {name}", i + 1);
    }
}
Output
#1 Ana
#2 Ben
#3 Chloe
place 1: Ana
place 2: Ben
place 3: Chloe

21. Changing elements

The whole array must be mut to change any element.

Rust
fn main() {
    let mut stock = [10, 0, 7];
    stock[1] = 25;
    stock[2] -= 1;
    println!("{:?}", stock);
}
Output
[10, 25, 6]

22. Arrays are values, not references

A Rust array is a plain value: assigning it copies all the elements.

Rust
fn main() {
    let original = [1, 2, 3];
    let mut copy = original;
    copy[0] = 100;
    println!("original: {:?}", original);
    println!("copy:     {:?}", copy);
}
Output
original: [1, 2, 3]
copy:     [100, 2, 3]

The same happens when you pass an array to a function: the function gets its own copy. Arrays of simple types are copied, like the numbers inside them. The exact rule, the Copy trait, is covered in Move, Copy & Clone.

23. The length is part of the type

A function that takes [i32; 3] only accepts arrays of exactly three elements.

Rust✗ does not compile · E0308
fn average(values: [i32; 3]) -> i32 {
    (values[0] + values[1] + values[2]) / 3
}

fn main() {
    let readings = [10, 20, 30, 40];
    println!("{}", average(readings));
}
Output
error[E0308]: mismatched types
 --> src/main.rs:7:28
  |
7 |     println!("{}", average(readings));
  |                    ------- ^^^^^^^^ expected an array with a size of 3, found one with a size of 4
  |                    |
  |                    arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn average(values: [i32; 3]) -> i32 {
  |    ^^^^^^^ ----------------

To accept arrays of any length, functions take a slice &[i32] instead. Slices are introduced in Slices, and this limitation is why most functions use them.

24. Comparing and searching arrays

Rust
fn main() {
    let expected = [1, 2, 3];
    let actual = [1, 2, 3];
    println!("equal: {}", expected == actual);

    let banned_ports = [23, 135, 445];
    let requested = 445;
    println!("port {requested} banned: {}", banned_ports.contains(&requested));
}
Output
equal: true
port 445 banned: true

25. Sorting and reversing an array

Rust
fn main() {
    let mut response_times_ms = [120, 45, 300, 87, 45, 210];
    response_times_ms.sort();
    println!("sorted: {:?}", response_times_ms);
    println!("median: {}", response_times_ms[response_times_ms.len() / 2]);
    response_times_ms.reverse();
    println!("slowest first: {:?}", response_times_ms);
}
Output
sorted: [45, 45, 87, 120, 210, 300]
median: 120
slowest first: [300, 210, 120, 87, 45, 45]

26. Two-dimensional arrays

An array of arrays forms a grid. The type [[i32; 3]; 2] means "2 rows of 3 columns".

Rust
fn main() {
    let mut grid: [[i32; 3]; 2] = [[1, 2, 3], [4, 5, 6]];
    grid[1][2] = 60;
    for row in grid {
        for cell in row {
            print!("{cell:4}");
        }
        println!();
    }
}
Output
   1   2   3
   4   5  60

27. Realistic example: weekly temperature statistics

Rust
fn main() {
    let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
    let highs = [18.5, 21.0, 19.2, 24.8, 26.1, 23.4, 17.9];

    let mut warmest = 0;
    let mut coldest = 0;
    let mut sum = 0.0;
    for i in 0..highs.len() {
        sum += highs[i];
        if highs[i] > highs[warmest] {
            warmest = i;
        }
        if highs[i] < highs[coldest] {
            coldest = i;
        }
    }
    let average = sum / highs.len() as f64;

    println!("Warmest: {} ({}°C)", days[warmest], highs[warmest]);
    println!("Coldest: {} ({}°C)", days[coldest], highs[coldest]);
    println!("Average: {average:.1}°C");
}
Output
Warmest: Fri (26.1°C)
Coldest: Sun (17.9°C)
Average: 21.6°C

28. Realistic example: checking a tic-tac-toe winner

A 3x3 grid of characters, plus an array of the 8 winning lines. Each line is an array of three (row, column) tuples.

Rust
fn winner(board: [[char; 3]; 3]) -> char {
    let lines = [
        [(0, 0), (0, 1), (0, 2)], [(1, 0), (1, 1), (1, 2)], [(2, 0), (2, 1), (2, 2)],
        [(0, 0), (1, 0), (2, 0)], [(0, 1), (1, 1), (2, 1)], [(0, 2), (1, 2), (2, 2)],
        [(0, 0), (1, 1), (2, 2)], [(0, 2), (1, 1), (2, 0)],
    ];
    for [(r1, c1), (r2, c2), (r3, c3)] in lines {
        let first = board[r1][c1];
        if first != '.' && first == board[r2][c2] && first == board[r3][c3] {
            return first;
        }
    }
    '-'
}

fn main() {
    let board = [
        ['X', 'O', 'X'],
        ['O', 'X', 'O'],
        ['.', 'O', 'X'],
    ];
    for row in board {
        println!("{} {} {}", row[0], row[1], row[2]);
    }
    println!("winner: {}", winner(board));
}
Output
X O X
O X O
. O X
winner: X

Notice the for loop destructures each line into three tuples in one pattern: [(r1, c1), (r2, c2), (r3, c3)]. Patterns can take apart arrays as well as tuples, and you will see much more of this in Pattern Matching.

29. Realistic example: a monthly sales report

Rust
fn main() {
    let regions = ["North", "South", "East", "West"];
    // rows: regions, columns: Jan, Feb, Mar
    let sales: [[u32; 3]; 4] = [
        [120, 135, 150],
        [98, 87, 110],
        [143, 160, 171],
        [75, 90, 64],
    ];

    println!("{:<8}{:>6}{:>6}{:>6}{:>8}", "Region", "Jan", "Feb", "Mar", "Total");
    let mut month_totals = [0; 3];
    for (r, region) in regions.iter().enumerate() {
        let mut total = 0;
        print!("{region:<8}");
        for month in 0..3 {
            print!("{:>6}", sales[r][month]);
            total += sales[r][month];
            month_totals[month] += sales[r][month];
        }
        println!("{total:>8}");
    }
    let grand: u32 = month_totals[0] + month_totals[1] + month_totals[2];
    println!("{:<8}{:>6}{:>6}{:>6}{:>8}", "All", month_totals[0], month_totals[1], month_totals[2], grand);
}
Output
Region     Jan   Feb   Mar   Total
North      120   135   150     405
South       98    87   110     295
East       143   160   171     474
West        75    90    64     229
All        436   472   495    1403

Summary

  • A tuple groups a fixed number of values of any types: (i32, &str, bool). Access with .0, .1, or destructure with let (a, b) = t;.
  • Tuples are the lightweight way to return several values from a function.
  • An array holds a fixed number of values of one type: [i32; 5]. The length is part of its type.
  • Indexing is bounds-checked: a compile error or a panic, never undefined behavior.
  • Arrays are values. Assigning or passing one copies it.
  • For a list that can grow, use Vec, coming up next.

Next: A First Look at String, Vec & HashMap.