What do you call an incredibly attractive tuple?
Irresistuple.
Sometimes a function needs to return two things. Coming up with a type name for that is not always worth it.
Rust has support for tuples, which are fixed-size groups of values. Unlike a
Vec, the elements can be different types, and the size is part of the type.
let response: (u16, &str, bool) = (200, "ok", true);
let pair = (1, 2); // type inferred as (i32, i32)
You access fields by index with a dot:
if response.2 {
println!("cached response: {}", response.0);
}
I prefer destructuring here so I can read status and cached rather than
remember what .0 and .2 mean. It pulls the parts out into named bindings in
one step.
let (status, message, cached) = response;
if cached {
println!("cached response: {status} {message}");
}
// Functions can return tuples for multiple values:
fn min_max(values: &[i32]) -> (i32, i32) {
(*values.iter().min().unwrap(), *values.iter().max().unwrap())
}
let (lo, hi) = min_max(&[3, 1, 4, 1, 5, 9]);
You only need a rough reading of the min_max body for now. These details are
enough to follow the example:
values.iter() walks the slice one element at a time. For now, read it as
"give me each element in turn.".min() / .max() return an Option (they'd return None for an empty
slice). .unwrap() says "I'm sure it's Some, give me the value or panic."* dereferences the &i32 the iterator hands back (the same
dereference you met in the hashmaps chapter), so we end
up with an owned i32 instead of a reference.When you only care about some fields, use _ to ignore the rest:
let (_, _, cached) = response;
Tuples are great for short-lived "two or three values that belong together"
situations. When the tuple grows or you keep passing the same tuple type around,
give those fields names with a struct instead.
Functions in Rust return a single value, but a tuple lets you bundle several values into that single return.
Here you'll return a (String, u32) pair: a name and an age.
Useful from the Standard Library
String::fromor.to_string()on a&strliteral gets you the ownedStringthe tuple wants in its first slot.
/// Returns a user's name and age as a tuple. For example, return "Alice"
/// and 25. Useful for functions that need to return multiple values.
fn get_user_info() -> (String, u32) {
("Alice".to_string(), 25)
}
#[test]
fn test_get_user_info() {
let (name, age) = get_user_info();
assert_eq!(name, "Alice");
assert_eq!(age, 25);
}
When two results are naturally produced together, returning them as a tuple is often clearer than two separate function calls. The caller destructures the result into named bindings.
* and + are all you need here. The dimensions in
the tests keep both u32 results within range.(area, perimeter). The return type
(u32, u32) tells the compiler to expect two u32 values.let (area, perimeter) = ... to pull the two
values back out into named bindings.
/// Calculates both area and perimeter of a rectangle. Returns (area, perimeter)
/// as a tuple.
fn rectangle_measurements(width: u32, height: u32) -> (u32, u32) {
let area = width * height;
let perimeter = 2 * (width + height);
(area, perimeter)
}
#[test]
fn test_rectangle_measurements() {
let (area, perimeter) = rectangle_measurements(5, 3);
assert_eq!(area, 15); // 5 * 3
assert_eq!(perimeter, 16); // 2 * (5 + 3)
}
You can destructure a tuple right in the function parameter list, or inside the
body with a let binding. Either way, you pull out the pieces by position.
Ownership still applies when you destructure. Passing a tuple of Strings by
value moves the whole tuple into the function, so the caller cannot use it
afterward. A tuple of integers is Copy, which gives the function its own copy
and leaves the caller's value usable. This is the same move-versus-copy
distinction you worked through in the moves chapter.
Useful Resources
- Rust by Example: destructuring tuples shows tuple patterns in
matcharms.- Field-by-index access (
full_name.0) also works, but a destructure with a meaningful name likefirstmakes the function body easier to read.
/// Extracts the first and last names from a full name tuple. Takes a tuple
/// (`first_name`, `last_name`) and returns just the first name.
///
/// Note: this signature moves the tuple in (it contains `String`s, which aren't
/// `Copy`). After the call, the original `full_name` is no longer valid; try
/// using it after calling this function and read the error. `swap_values` below
/// takes `(i32, i32)`, which is `Copy`, so the original is still usable.
/// Chapter 12 covers this properly.
///
/// Hint: Use [tuple
/// destructuring](https://doc.rust-lang.org/rust-by-example/flow_control/match/destructuring/destructure_tuple.html)
fn get_first_name(full_name: (String, String)) -> String {
let (first_name, _last_name) = full_name;
first_name
}
#[test]
fn test_get_first_name() {
let full_name = ("John".to_string(), "Doe".to_string());
assert_eq!(get_first_name(full_name), "John");
}
Return the two values in reverse order, using tuple destructuring.
This tuple contains only Copy values. After the tests pass, try using the
original pair after passing it to swap_values. Why does that work, while
using the original name tuple after get_first_name does not?
/// Swaps two values using tuple destructuring.
fn swap_values(pair: (i32, i32)) -> (i32, i32) {
let (first, second) = pair;
(second, first)
}
#[test]
fn test_swap_values() {
assert_eq!(swap_values((1, 2)), (2, 1));
assert_eq!(swap_values((42, 100)), (100, 42));
}
You used tuples to return multiple values, destructured them in parameter lists
and let bindings, and saw how ownership behaves differently for Copy and
non-Copy element types.
What We Learned
- A tuple is a fixed-size group of values whose size and per-slot types are part of the type.
(String, u32)and(u32, String)are different types.- Build a tuple with parentheses; access fields with
.0,.1, etc. Destructuring withlet (a, b) = pair;is usually clearer.- Tuples are the lightest-weight way to return more than one value from a function. When the same tuple shows up in many places or grows past two or three fields, a
structcan give the fields names.- Use
_in a pattern to ignore a field:let (first, _) = pair;does not move the second field.- Fields bound by value are moved or copied according to their types:
Stringfields move, while integer fields copy.- The unit type
()is the empty tuple. It's what functions "without a return value" actually return.
Extra practice to explore at your own pace. These chapters do not count toward course progress.