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 we haven't formally
introduced yet. 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 that the iterator hands
back, 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 (chapter 12).
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.
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.
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 chapter 11 covers 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. Chapter 11 covers move semantics in depth.
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.
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(chapter 12).- 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.