Rust (2024 edition) Module 2 – Values, types, functions and control flow
Tuples, arrays and type conversions
Group values in tuples and arrays, see what an index past the end does, and convert numbers with as, From and TryFrom, knowing what each can lose.
What you will learn
- Create, read and destructure tuples, and swap two values with one tuple assignment
- Create fixed-size arrays, read them safely with get, and explain what an index out of range does
- Convert between number types with as, From and TryFrom, and predict when as loses information
- Choose the conversion that fits a value, and report the value that did not fit
Before you start
What you’ll learn · Before you start
What you will learn
- Create, read and destructure tuples, and swap two values with one tuple assignment
- Create fixed-size arrays, read them safely with get, and explain what an index out of range does
- Convert between number types with as, From and TryFrom, and predict when as loses information
- Choose the conversion that fits a value, and report the value that did not fit
Before you start
On this page
300i32 as u8 is 44, and rustc builds that line without a warning. Rust never turns one number type into another on
its own, but how you ask decides what can be lost: as always produces a value, From exists only where nothing can
be lost, and TryFrom checks and can fail.
First, though, the lesson groups values: in tuples, which mix types, and in arrays, whose length is part of their type and whose every index is checked. Every result on the page is a run on Rust 1.99.0.
A tuple groups a fixed number of values of any types
A post office’s city and postal code belong together but have different types, a &str and a u32. A
tuple: A value that groups a fixed number of values, each of its own type, such as (&str, u32). Its values are read by position, .0 and .1, or taken apart all at once with a pattern. holds both:
// A tuple groups a fixed number of values, each of its own type.
fn main() {
let office = ("Pune", 411_001u32);
println!("{} has postal code {}", office.0, office.1);
let (city, code) = office;
println!("destructured: {city}, {code}");
let mut first = "Asha";
let mut second = "Ravi";
(first, second) = (second, first);
println!("after the swap: {first}, {second}");
let nothing = ();
println!("the unit value: {nothing:?}, {} bytes", std::mem::size_of_val(¬hing));
println!("the whole tuple: {office:?}");
} Output
Pune has postal code 411001
destructured: Pune, 411001
after the swap: Ravi, Asha
the unit value: (), 0 bytes
the whole tuple: ("Pune", 411001)
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O tuples.rs && ./tuples
Rust tuples have no field names: .0 and .1 read the values by position. let (city, code) = office;
destructures the tuple into two bindings at once, and the same pattern on the left of an assignment swaps two
values in one line, (first, second) = (second, first). Rust tuples of up to twelve values print with {:?} and
compare with ==; the last line printed one.
The empty tuple, (), is called the unit type: The type of the empty tuple, written (), with exactly one value, also written (). It takes no memory, and it is what a function returns when it returns nothing.. It has one value, also written (),
takes no bytes, and is what a function returns when it returns nothing, which the next lesson shows.
An array’s length is part of its type, and every index is checked
An array holds a fixed number of values of one type, and its type says how many: four marks are a [u32; 4].
[0u8; 8] repeats a value eight times:
// An array: a fixed number of values of one type, with the length in its type.
fn main() {
let marks: [u32; 4] = [72, 85, 64, 91];
let empty_row = [0u8; 8];
println!("{} marks: {marks:?}", marks.len());
println!("a row of {} empty seats: {empty_row:?}", empty_row.len());
let mut copy = marks;
copy[0] = 100;
println!("marks: {marks:?}, copy: {copy:?}");
let mut total = 0;
for mark in marks {
total += mark;
}
println!("total: {total}");
println!("marks.get(2) = {:?}, marks.get(9) = {:?}", marks.get(2), marks.get(9));
} Output
4 marks: [72, 85, 64, 91] a row of 8 empty seats: [0, 0, 0, 0, 0, 0, 0, 0] marks: [72, 85, 64, 91], copy: [100, 85, 64, 91] total: 312 marks.get(2) = Some(64), marks.get(9) = None
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O arrays.rs && ./arrays
Assigning an array copies it when its values can be copied, as numbers can, so changing copy left marks as it
was. for mark in marks takes each value in turn, and get(9) answered None for a position past the end instead of
stopping the program. Rust arrays never grow; a list whose length changes is a Vec, which the collections module
covers. Indexing with [] is checked too. When the compiler can see the index, it refuses a bad one:
// An index the compiler can check.
fn main() {
let marks = [72, 85, 64, 91];
println!("the fifth mark: {}", marks[4]);
} Output (exit status 1)
Printed as an error (standard error)
error: this operation will panic at runtime
--> const_index.rs:4:36
|
4 | println!("the fifth mark: {}", marks[4]);
| ^^^^^^^^ index out of bounds: the length is 4 but the index is 4
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to 1 previous error
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O const_index.rs && ./const_index
When the index is only known while the program runs, the check runs then. This loop, meant to add up the four marks, runs one position too far:
// Meant to add up four marks, with a range that runs one position too far.
fn main() {
let marks = [72, 85, 64, 91];
let mut total = 0;
for i in 0..=marks.len() {
total += marks[i];
}
println!("total: {total}");
} from session import run
# sum_marks.rs builds, because the index is only known while it runs; the run stops at the bad index.
run("""
rustc --edition 2024 sum_marks.rs
!./sum_marks
""") Output
$ rustc --edition 2024 sum_marks.rs $ ./sum_marks thread 'main' (…) panicked at sum_marks.rs:6:18: index out of bounds: the len is 4 but the index is 4 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace [exit status 101]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 index_session.py
Step through the loop and the check that stopped it:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Five positions, four values.
0..=marks.len()is the range 0 to 4 with 4 included: it gives five positions, 0, 1, 2, 3 and 4, for an array of four values. - Position 0.
marks[0]first goes to the bounds check: 0 is below the length, 4, so the read goes ahead. - The read. The value at position 0 is read and added: the total is 72.
- Positions 1 to 3. Positions 1, 2 and 3 pass the same check and add 85, 64 and 91. The total is 312, the right answer, but the range is not finished.
- Position 4.
marks[4]reaches the check, and 4 is not below 4. The check stops the program with a panic before anything past the array is read, sototal: …is never printed. - The fix.
0..marks.len()leaves 4 out, so the loop reads positions 0 to 3. Better still,for mark in marksreads each value with no index to get wrong.
That bounds check: The check that an index is inside a list before the value there is read. In safe Rust a failed check stops the program with a panic, so nothing past the end is ever read. makes a read past the end impossible in safe Rust: the program stops with a panic: Rust's way of stopping a program, or one thread of it, at an error it cannot carry on from, such as an index past the end of a list. It prints the reason and the line, and a program whose main thread panics exits with status 101. More in “Why Rust: safety, speed and where it runs” instead. Here are tuples and arrays side by side:
| Property | Tuple | Array |
|---|---|---|
| Values | any types, each its own | all of one type |
| Length | fixed, part of the type | fixed, part of the type |
| Read a value | .0, .1: positions fixed in the code |
[i] or get(i): a position worked out while running |
| Loop over it | no | yes |
| Typical use | a few values that belong together | many values of the same kind |
Checkpoint
Check: Tuples and destructuring · Arrays and checked indexes
Number types never convert by themselves
Rust has no implicit conversions between number types, not even to a wider one where nothing could be lost. This
program assigns a u8 to a u32 and a u32 to a u8:
// A wider and a narrower number type, with no conversion written.
fn main() {
let seats: u8 = 200;
let booked: u32 = 1_000;
let total: u32 = seats;
let shown: u8 = booked;
println!("{total} {shown}");
} Output (exit status 1)
Printed as an error (standard error)
error[E0308]: mismatched types --> no_implicit.rs:5:22 | 5 | let total: u32 = seats; | --- ^^^^^ expected `u32`, found `u8` | | | expected due to this | help: you can convert a `u8` to a `u32` | 5 | let total: u32 = seats.into(); | +++++++ error[E0308]: mismatched types --> no_implicit.rs:6:21 | 6 | let shown: u8 = booked; | -- ^^^^^^ expected `u8`, found `u32` | | | expected due to this | help: you can convert a `u32` to a `u8` and panic if the converted value doesn't fit | 6 | let shown: u8 = booked.try_into().unwrap(); | ++++++++++++++++++++ error: aborting due to 2 previous errors For more information about this error, try `rustc --explain E0308`.
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O no_implicit.rs && ./no_implicit
Both lines are E0308, mismatched types, and each help line suggests one of Rust’s type conversions for its
direction. For the wider type, .into(): a conversion that cannot fail. For the narrower one, .try_into(), which
can fail, with .unwrap() to stop the program if it does. These are the three type conversions for numbers:
asconverts any number type to any other and never fails, but it may lose information.From, or.into(), exists only where every value fits, so it cannot lose information.TryFrom, or.try_into(), checks the value and returns an error when it does not fit.
Here are all three on values that fit and values that do not:
// One value converted three ways: From, TryFrom and as.
fn main() {
let small: u8 = 200;
let big: i32 = 300;
println!("u32::from(200u8) = {}", u32::from(small));
println!("u8::try_from(300i32) = {:?}", u8::try_from(big));
println!("300i32 as u8 = {}", big as u8);
} Output
u32::from(200u8) = 200 u8::try_from(300i32) = Err(TryFromIntError(PosOverflow)) 300i32 as u8 = 44
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O conversions.rs && ./conversions
u32::from(200u8) gave 200, because every u8 fits a u32. u8::try_from(300i32) gave an error that even says
which way it overflowed. 300i32 as u8 gave 44 and no error at all: the value silently became another number.
Key idea
Number types never convert on their own: write From or .into() where every value fits, TryFrom where one may
not, and as only where you have decided what a value that does not fit should become.
Checkpoint
Check: as, From and TryFrom
Every kind of as cast, measured, and what each loses Pro: opens with a Pro, Premium or Ultimate pass
as converts between any two number types, and between numbers, chars and bools, and it never fails. This part runs
every kind of cast, shows what happens to the bits, and ends with when as is still the right tool.
In this part:
- What an integer cast does to the bits, cast by cast
- Floats cast to integers: too large, too small and NaN
- Integers cast to floats, and chars cast to numbers
- When as is the right tool, and what to use instead
1 diagram · 2 runnable examples (Rust) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
From and TryFrom in practice, with an exercise Pro: opens with a Pro, Premium or Ultimate pass
From and TryFrom turn a conversion into something the compiler checks: lossless, or checked with an error that
says what went wrong. This part shows where each exists, what their errors say, what newer releases added, and an
exercise that converts four values or names the one that failed.
In this part:
- From exists only where nothing can be lost
- TryFrom returns an error that says which way it failed
- Three ways to handle a conversion that can fail
- Conversions that newer releases added
- Exercise: four values into a [u8; 4]
1 diagram · 4 runnable examples (Rust) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: tuples, arrays and conversions
Questions on tuples, arrays, bounds checks and the three conversions; each answer you pick is explained at once.
Programmer Calculator (Bitwise) Type 300 and look at its low 8 bits, the 44 that as u8 keeps.Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): what is the difference between a tuple and an array in Rust?
Both have a fixed length that is part of their type. A tuple groups a few values that may each have their own type,
such as (&str, u32), reached by positions written in the code, .0 and .1, or by destructuring. An array holds
values of one type, [u32; 4], so it can be indexed with a position worked out while the program runs and looped
over, with every index checked. A tuple suits a few values that belong together, such as two results of a function;
an array, or a Vec if the length changes, suits many values of one kind.
-
Fresher to mid
An upload service stores each file's size, a u64 from the file system, in a column it fills with
size as i32. For one 3 GiB file the dashboard shows a negative size, and the quota check let the upload through. What happened, and what do you change? -
Mid to senior
An HTTP handler parses
?slot=from a request into an i64 and returnsslots[slot as usize]from an array of 48 time slots. A crafted request makes the handler panic. What happened, and how would you write the lookup? -
Fresher to mid
A function returns
(u32, u32, bool): the bytes read, the lines read and whether the input ended early. After a refactor, a caller swaps the two u32 values and nobody notices for a week. What would you change, and when is a tuple still the right return type?
Key takeaways
- Rust tuples group a fixed number of values of any types, read with
.0and.1or by destructuring, and()is the empty tuple. - An array’s length is part of its type;
getreturnsNonepast the end, and a[]index out of range is refused when rustc can see it and panics when it cannot. - Number types never convert by themselves; of the three type conversions,
asnever fails but can lose information,Fromexists only where nothing is lost, andTryFromchecks.
Terms in this lesson
- bounds check
- The check that an index is inside a list before the value there is read. In safe Rust a failed check stops the program with a panic, so nothing past the end is ever read.
- tuple
- A value that groups a fixed number of values, each of its own type, such as (&str, u32). Its values are read by position, .0 and .1, or taken apart all at once with a pattern.
- unit type
- The type of the empty tuple, written (), with exactly one value, also written (). It takes no memory, and it is what a function returns when it returns nothing.
References
- The Rust Programming Language: Data Types (compound types) (The Rust Project)
- The Rust Reference: Tuple types (The Rust Project)
- The Rust Standard Library: tuple (The Rust Project)
- The Rust Reference: Array types (The Rust Project)
- The Rust Reference: Array and slice indexing expressions (The Rust Project)
- The Rust Reference: Type cast expressions (The Rust Project)
- The Rust Standard Library: From (The Rust Project)
- The Rust Standard Library: TryFrom (The Rust Project)
- The Rust Standard Library: TryFromIntError (The Rust Project)
- The Rust Standard Library: slice::get (The Rust Project)
- The Rust Edition Guide: IntoIterator for arrays (The Rust Project)
- The Rust Edition Guide: Rust 2021 (The Rust Project)
- The rustc book: Deny-by-default lints (The Rust Project)
- Clippy lints (The Rust Project)
- Announcing Rust 1.34.0 (The Rust Project)
- Announcing Rust 1.45.0 (The Rust Project)
- Announcing Rust 1.59.0 (The Rust Project)
- Announcing Rust 1.87.0 (The Rust Project)
- Announcing Rust 1.94.0 (The Rust Project)
- Announcing Rust 1.95.0 (The Rust Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress