Lesson 4 of 22·14 min

Arrays, Tuples and the Unit Type

The primitive types each hold one value. Most programs need to keep a few values together: a point's x and y, the days of a week, a name and a score. Rust has two built-in compound types for that, and they are the subject of this lesson:

  • a tuple groups a fixed number of values that can have different types;
  • an array holds a fixed number of values that all have the same type.

Along the way you will meet the unit type (), the empty tuple, which Rust uses whenever there is no meaningful value to return.

Tuples

A tuple is a comma-separated list of values in parentheses. Its type is the list of its element types, in order:

fn main() {
    let player: (&str, u32, bool) = ("Ferris", 1200, true);
    let origin = (0.0, 0.0);
    println!("{player:?} {origin:?}");
}
("Ferris", 1200, true) (0.0, 0.0)

("Ferris", 1200, true) has the type (&str, u32, bool). Like any variable, you can write the type out or let the compiler infer it. The length is part of the type too: a two-element tuple and a three-element tuple are different types, and a tuple can never grow or shrink.

Tuples don't implement Display, so print them with {:?}, like above.

Reading the fields

Access a tuple's elements by position with a dot and a number, starting at 0:

fn main() {
    let mut player = ("Ferris", 1200, true);
    println!("{} has {} points", player.0, player.1);
 
    player.1 += 50;
    player.2 = false;
    println!("{player:?}");
}
Ferris has 1200 points
("Ferris", 1250, false)

If the tuple is mut, you can assign to its fields as well. The field number is fixed when you write the code: player.1 is fine, but there is no way to pick a tuple field with a variable.

Destructuring

Often the nicest way to use a tuple is to take it apart into separate variables with a pattern on the left side of let:

fn main() {
    let player = ("Ferris", 1200, true);
    let (name, points, online) = player;
    println!("{name}: {points} points, online: {online}");
 
    let (city, _, temperature) = ("Lisbon", "Portugal", 24);
    println!("{city} is {temperature}°C");
}
Ferris: 1200 points, online: true
Lisbon is 24°C

The pattern has the same shape as the tuple, and each name is bound to the value in the same position. _ skips a value you don't need. Destructuring also makes swapping two variables a one-liner: (a, b) = (b, a);.

Returning several values

Tuples are the usual way for a function to return more than one value. You will write functions properly in a later lesson, but the idea is simple: the return type is a tuple type, and the function ends with a tuple:

fn split_duration(total_seconds: u32) -> (u32, u32) {
    (total_seconds / 60, total_seconds % 60)
}
 
fn main() {
    let (minutes, seconds) = split_duration(135);
    println!("{minutes} min {seconds} s");
}
2 min 15 s

The caller usually destructures the result straight away, so each part gets a meaningful name.

Tuples are good for small, short-lived groups of values. Once a group has more than two or three fields, or gets passed around a lot, .0 and .2 stop meaning much. That is when you define a struct with named fields, which has its own lesson later.

The unit type

A tuple with no elements, (), is called the unit type. It has exactly one value, also written (). It carries no information, and that is what it is for: it is the value of things that don't produce a value.

A function without a -> return type returns (). These two functions are the same:

fn log_start() {
    println!("starting up");
}
 
fn log_stop() -> () {
    println!("shutting down");
}
 
fn main() {
    let a = log_start();
    let b = log_stop();
    println!("{a:?} {b:?}");
}
starting up
shutting down
() ()

You would normally leave -> () out; it is shown here so you can see what the short form means. main itself returns (), as does println!.

() also appears in values you will meet in later lessons. Result<(), String> means "either success with nothing to report, or an error message", and a HashMap<String, ()> is a map that only cares about its keys.

Because () is a real value with a real type, it also shows up in error messages. When the compiler says it found (), it usually means a block or function ended in a statement instead of a value; the common mistakes section below has the classic case.

Arrays

An array is a fixed-size list of values of the same type, written in square brackets:

fn main() {
    let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri"];
    let readings: [f64; 4] = [20.5, 21.0, 19.8, 22.3];
    let zeros = [0u8; 6];
    println!("{weekdays:?}");
    println!("{readings:?}");
    println!("{zeros:?}");
}
["Mon", "Tue", "Wed", "Thu", "Fri"]
[20.5, 21.0, 19.8, 22.3]
[0, 0, 0, 0, 0, 0]

An array's type is written [T; N]: the element type and the length. readings is a [f64; 4], four f64 values. [0u8; 6] is the short form for "six copies of 0u8", handy for starting with a buffer of zeros.

The length is part of the type and is fixed at compile time. A [f64; 4] always has exactly four elements; you can't push a fifth. When you need a list that grows, use a Vec, covered in the collections lesson. Arrays are for things that really have a fixed size: the days of a week, the cells of a 3 by 3 board, the RGB channels of a colour.

Indexing

Read and write elements with [index]. Indexes start at 0 and are usize:

fn main() {
    let mut board = ['.'; 9];
    board[4] = 'X';
    board[0] = 'O';
    println!("first: {}, centre: {}", board[0], board[4]);
    println!("cells: {}", board.len());
    println!("{:?}", board.first());
    println!("{}", board.contains(&'X'));
}
first: O, centre: X
cells: 9
Some('O')
true

To change an element, the array itself must be mut. .len() gives the number of elements; .first() and .last() give the first and last element if there is one (the Some(...) is an Option, covered later). contains takes a reference, hence the &; references get their own lesson too.

Arrays can hold arrays. A 3 by 3 grid is an array of three rows, each an array of three cells:

fn main() {
    let grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
    println!("{}", grid[1][2]);
    println!("{:?}", grid[2]);
}
6
[7, 8, 9]

Looping over an array

To do something with every element, use a for loop. Loops get a lesson of their own; for now, for item in array runs the body once per element:

fn main() {
    let temperatures = [18, 23, 31, 27, 16];
    let mut hot_days = 0;
    for t in temperatures {
        if t > 25 {
            hot_days += 1;
        }
    }
    println!("{hot_days} hot days out of {}", temperatures.len());
}
2 hot days out of 5

Iterating like this is safer than counting indexes yourself, because there is no index to get wrong. Arrays also have iterator methods that do common jobs in one call; the closures and iterators lesson covers them.

Passing arrays to functions

A function can take an array with a fixed length, like [i32; 5], but then it only accepts arrays of exactly that length. More often a function takes a slice, &[i32], which is a view into any number of i32s in a row. An array of any length can be passed as a slice with &:

fn describe(values: &[i32]) {
    println!("{} values, first is {:?}", values.len(), values.first());
}
 
fn main() {
    let small = [7, 8];
    let large = [1, 2, 3, 4, 5, 6];
    describe(&small);
    describe(&large);
    describe(&large[2..4]);
}
2 values, first is Some(7)
6 values, first is Some(1)
2 values, first is Some(3)

&large[2..4] is a slice of just the elements at indexes 2 and 3. Slices have their own lesson after ownership and borrowing; for now it is enough to know that &[T] is how you accept "some number of Ts" in a function.

Common mistakes

Indexing past the end

The last valid index of an array is len() - 1. Going one further is the classic off-by-one bug, here from a loop that runs up to and including the length:

fn main() {
    let scores = [90, 72, 85];
    for i in 0..=scores.len() {
        println!("{}", scores[i]);
    }
}

Rust checks every index at runtime and panics instead of reading memory that isn't part of the array. (When the index is a constant, like scores[5], the compiler catches it before the program even runs.) Use 0..scores.len(), or better, loop over the elements directly with for score in scores.

Arrays of different lengths

The length is part of an array's type, so arrays of different lengths are different types:

fn main() {
    let mut rgb: [u8; 3] = [255, 128, 0];
    rgb = [0, 0];
    println!("{rgb:?}");
}

If the number of elements really changes, you want a Vec, not an array.

A tuple field that doesn't exist

fn main() {
    let point = (3, 4);
    println!("{}", point.2);
}

Tuple fields count from 0, so a pair has .0 and .1.

A trailing semicolon that returns ()

A function returns the value of its last expression. Put a semicolon after it and it becomes a statement, and the function returns () instead:

fn average(total: u32, count: u32) -> u32 {
    total / count;
}
 
fn main() {
    println!("{}", average(90, 3));
}

The compiler points at the semicolon: remove it and total / count becomes the function's return value. The functions lesson explains expressions and statements properly.

Summary

  • A tuple (T1, T2, ...) groups a fixed number of values of any types. Read fields with .0, .1, or destructure with let (a, b) = pair;.
  • Tuples are the usual way to return a few values from a function. When the fields need names, use a struct.
  • () is the unit type: the empty tuple, with one value, also (). Functions without a return type return it.
  • An array [T; N] holds exactly N values of type T. [x; N] repeats one value; the length is part of the type.
  • Arrays are indexed from 0 with usize. An out-of-range index panics.
  • Loop over an array with for item in array, and take &[T] in functions so they accept arrays of any length.

Practise what you read

Challenges that use the ideas from this lesson.