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°CThe 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 sThe 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')
trueTo 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 5Iterating 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 withlet (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 exactlyNvalues of typeT.[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.