Ir al contenido

Tuplas en Rust

Una tupla agrupa varios valores, que pueden ser de tipos distintos, en uno solo. Es una forma rápida de juntar cosas relacionadas sin tener que definir un tipo nuevo para ellas.

fn main() {
let person: (&str, i32, bool) = ("Alicia", 30, true);
println!("{:?}", person);
}

Salida:

("Alicia", 30, true)
▶ Pruébalo tú mismo

Fíjate en que la tupla mezcla un &str, un i32 y un bool en un solo valor. Un array no puede hacer eso, porque todos sus elementos tienen que ser del mismo tipo.

Para imprimir una tupla entera, usa {:?} (formato de depuración) en lugar de {}. Un {} a secas no funciona con tuplas.

Usa .0, .1, .2, etc. Las tuplas se numeran empezando desde cero:

fn main() {
let person = ("Alicia", 30, true);
println!("Nombre: {}", person.0);
println!("Edad: {}", person.1);
println!("Activa: {}", person.2);
}

Salida:

Nombre: Alicia
Edad: 30
Activa: true
▶ Pruébalo tú mismo

La posición tiene que ser un número escrito tal cual en el código. No puedes usar una variable, como person.i, porque cada posición puede guardar un tipo distinto, y Rust tiene que conocer el tipo al compilar.

Puedes sacar cada valor a su propia variable en una sola línea:

fn main() {
let person = ("Alicia", 30, true);
let (name, age, is_active) = person;
println!("{name} tiene {age} años, activa: {is_active}");
}

Salida:

Alicia tiene 30 años, activa: true
▶ Pruébalo tú mismo

Esto suele ser más claro que escribir .0, .1, .2 una y otra vez, sobre todo cuando la tupla tiene más de dos o tres campos.

Las tuplas son la forma natural de devolver más de un valor, ya que una función solo puede declarar un tipo de retorno:

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
fn main() {
let (smallest, largest) = min_max(&[4, 8, 1, 9, 3]);
println!("mínimo: {smallest}, máximo: {largest}");
}

Salida:

mínimo: 1, máximo: 9
▶ Pruébalo tú mismo

No te preocupes todavía por &[i32], .iter() o .unwrap(). Los verás en las páginas de Slices, Iteradores y Option. Lo que importa aquí es el tipo de retorno (i32, i32) y cómo main lo desestructura enseguida en dos variables.

Si declaras una tupla con mut, puedes cambiar sus campos uno a uno, usando las mismas posiciones .0, .1. Los valores pueden cambiar, pero la forma no: una tupla siempre conserva el número y los tipos de campos con los que empezó.

La desestructuración también funciona al revés: puedes asignar a variables que ya existen. Así consigues un intercambio limpio en una sola línea, sin variable temporal. Si solo quieres algunos de los campos, escribe _ en los que no necesites:

fn main() {
let mut point = (3, 4);
point.0 += 10; // cambia un campo directamente
println!("point es {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // intercambia los dos valores
println!("a = {a}, b = {b}");
let (x, _) = point; // nos quedamos con x e ignoramos el segundo campo
println!("x = {x}");
}

Salida:

point es (13, 4)
a = 2, b = 1
x = 13
▶ Pruébalo tú mismo

El intercambio funciona porque Rust construye primero el lado derecho, (b, a). Solo después lo desempaqueta en a y b. Puedes hacer mucho más con _ y los patrones en la página de Coincidencia de patrones.

En realidad ya conociste una tupla sin darte cuenta. () se llama tipo unitario (unit type), y es lo que devuelve una función cuando no devuelve nada útil. Es el tipo de retorno implícito de fn main().

Las tuplas son ideales para agrupar cosas rápido y sin nombres, sobre todo para devolver dos o tres valores desde una función. Cuando un grupo de valores representa un concepto real con campos con nombre (como una Person con nombre y edad a los que te vas a referir por su nombre en todo tu código), un struct suele ser la opción mejor y más legible.

Escribe una función sum_and_product(a: i32, b: i32) -> (i32, i32) que devuelva a la vez la suma y el producto de dos números. En main, llámala con 5 y 6 y desestructura el resultado. Pista: la última línea de la función es la propia tupla, (a + b, a * b), sin punto y coma.

Ver solución
fn sum_and_product(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn main() {
let (sum, product) = sum_and_product(5, 6);
println!("suma = {sum}, producto = {product}");
}

Salida:

suma = 11, producto = 30
▶ Pruébalo tú mismo

Un rectángulo se puede guardar como una tupla de dos puntos, donde cada punto es a su vez una tupla: let rect = ((1, 2), (5, 5));. Calcula su ancho, su alto y su área. Pista: las tuplas se pueden anidar, y rect.1.0 significa “el primer campo del segundo punto”.

Ver solución
fn main() {
let rect = ((1, 2), (5, 5));
let width = rect.1.0 - rect.0.0;
let height = rect.1.1 - rect.0.1;
println!("ancho {width}, alto {height}, área {}", width * height);
}

Salida:

ancho 4, alto 3, área 12
▶ Pruébalo tú mismo

También puedes desestructurar los dos puntos a la vez: let ((x1, y1), (x2, y2)) = rect;. Hay que escribir más, pero x2 - x1 se lee mejor que rect.1.0 - rect.0.0. Cuando los puntos empiezan a acumularse así, es señal de que un struct con campos con nombre sería más claro.

  • Una tupla agrupa valores que pueden ser de tipos distintos: (name, age, is_active).
  • Accede a los campos con .0, .1, .2, o sácalos todos a la vez desestructurando.
  • Las tuplas son la forma natural de devolver varios valores desde una función.
  • Los campos de una tupla mut pueden cambiar, pero su tamaño y sus tipos no. (a, b) = (b, a); intercambia dos valores.
  • Para agrupaciones con nombre y reutilizables, usa mejor un struct.

Comprueba lo aprendido

1. ¿Qué puede guardar una tupla que un array no puede?

2. ¿Cómo accedes al segundo elemento de una tupla llamada person?

3. Después de let mut a = 1; let mut b = 2; (a, b) = (b, a);, ¿cuánto valen a y b?

Puntuación: 0 / 3