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]);
That min_max body has three pieces of syntax worth a quick note.
Don't let them trip you up here:
values.iter() walks the slice one element at a time.
Iterators get a full chapter later; 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."
Option is the next chapter.* 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 find yourself passing it around a lot, that's a hint to define a struct instead (the structs chapter).
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. Bothu32results fit easily for any sane rectangle.- 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.
Watch out for ownership: a tuple of Strings is moved into the function, while a tuple of integers is copied.
"Moved" means the caller's binding is no longer usable afterwards, because the value's single owner is now the function parameter rather than the caller.
"Copied" means the value is duplicated bit-for-bit, so the caller keeps theirs and the function gets its own.
The split is decided by a trait called Copy: types that are tiny and have no heap data (integers, bools, char, fixed-size arrays of those, and tuples made entirely of Copy types) implement it; types that own heap data (like String or Vec) deliberately don't.
The doc-comment below has more on this, and the moves chapter covered move semantics in depth.
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(likeString) is moved when bound by destructuring, so the caller's binding becomes unusable: ownership of the underlying heap buffer transferred into the function.Copytypes (integers, bools,char, and tuples of those) are duplicated instead, so the caller keeps their copy. After this function returns, the caller's(String, String)tuple is gone. The moves chapter covered this in depth.
/// 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, switch to a
struct(the structs chapter).- 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.