A tuple is a fixed-size group of values.
Unlike a Vec, the elements can be different types, and the size is part of the type.
let user: (String, u32) = ("Alice".to_string(), 25);
let pair = (1, 2); // type inferred as (i32, i32)
let triple = ("ok", 200, true); // (&str, i32, bool)
You access fields by index with a dot:
let name = user.0;
let age = user.1;
But the more idiomatic way is destructuring: pull the parts out into named bindings in one step.
let (name, age) = user;
let (a, b) = (1, 2);
// 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 (first, _) = ("Alice", "Smith");
Tuples are great for short-lived "two or three values that belong together" situations.
When the tuple grows or you keep passing the same shape 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. It's the lightest-weight way to hand back more than one thing without defining a new type.
Here you'll return a (String, u32) pair: a name and an age.
Useful from the standard library
- The Rust Book on tuples covers tuple syntax and how the type signature is just the parenthesized list of element types.
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.
Useful from the standard library
- The arithmetic operators
*and+are all you need here. The dimensions in the tests keep bothu32results within range.- Tuple construction is just parentheses:
(area, perimeter). The return type(u32, u32)already tells the compiler what shape to expect.- The caller in the test uses
let (area, perimeter) = ...to destructure the return into named bindings, the mirror image of how you build it.
/// 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 from the standard library
- Rust by Example: destructuring tuples shows the
let (a, b) = pair;form and how_can ignore parts you don't want to bind.- Field-by-index access (
full_name.0) also works, but a destructure with a meaningful name likefirstreads better at the call site.- Anything that isn't
Copy, such asString, moves when destructured by value. A tuple is onlyCopywhen all of its elements areCopy.
/// 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");
}
Tuple destructuring makes swapping two values a one-liner: bind the pair to (a, b) and return (b, a).
No temporary variable, no manual juggling.
Useful from the standard library
std::mem::swapswaps two&mut Treferences in place. Useful when you can't take ownership; here, returning a fresh tuple is cleaner.- The integers in this exercise are
Copy, so(b, a)makes bit-wise copies of both. No moves to worry about.
/// 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;.- Move vs. copy still applies: a tuple of
Strings moves on destructure, a tuple of integers copies. The element types decide.- The unit type
()is the empty tuple. It's what functions "without a return value" actually return.