Salta ai contenuti

Tuple in Rust

Una tupla raggruppa in un unico valore più valori, anche di tipi diversi. È un modo veloce per mettere insieme cose collegate, senza definire un tipo nuovo apposta.

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

Output:

("Alice", 30, true)
▶ Provalo tu

Nota che la tupla mescola in un solo valore un &str, un i32 e un bool: un array non può farlo, perché tutti i suoi elementi devono avere lo stesso tipo.

Per stampare una tupla intera usa {:?} (formattazione di debug) invece di {}. Con le tuple, la semplice {} non funziona.

Usa .0, .1, .2 e così via: le posizioni di una tupla partono da zero:

fn main() {
let person = ("Alice", 30, true);
println!("Nome: {}", person.0);
println!("Età: {}", person.1);
println!("Attivo: {}", person.2);
}

Output:

Nome: Alice
Età: 30
Attivo: true
▶ Provalo tu

La posizione deve essere un numero scritto direttamente nel codice. Non puoi usare una variabile, come person.i, perché ogni posizione può contenere un tipo diverso e Rust deve conoscere il tipo in fase di compilazione.

Puoi tirare fuori ogni valore in una sua variabile, in una riga sola:

fn main() {
let person = ("Alice", 30, true);
let (name, age, is_active) = person;
println!("{name} ha {age} anni, attivo: {is_active}");
}

Output:

Alice ha 30 anni, attivo: true
▶ Provalo tu

Spesso è più chiaro che ripetere .0, .1, .2, soprattutto quando una tupla ha più di due o tre campi.

Le tuple sono il modo naturale per restituire più di un valore, visto che una funzione può dichiarare un solo tipo di ritorno:

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!("min: {smallest}, max: {largest}");
}

Output:

min: 1, max: 9
▶ Provalo tu

Per ora non preoccuparti di &[i32], .iter() o .unwrap(): li trovi nelle pagine sulle Slice, sugli Iteratori e su Option. Qui la cosa da notare è il tipo di ritorno (i32, i32) e il modo in cui main lo destruttura subito in due variabili.

Se una tupla è dichiarata con mut, puoi cambiarne i campi uno alla volta, usando le stesse posizioni .0, .1. I valori possono cambiare, ma la forma no: una tupla mantiene sempre il numero e i tipi dei campi con cui è nata.

La destrutturazione funziona anche al contrario: puoi assegnare dentro variabili che esistono già. Così ottieni uno scambio elegante in una riga, senza variabile temporanea. Se ti servono solo alcuni campi, scrivi _ per quelli che non ti interessano:

fn main() {
let mut point = (3, 4);
point.0 += 10; // modifica un campo sul posto
println!("il punto è {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // scambia i due valori
println!("a = {a}, b = {b}");
let (x, _) = point; // tieni x, ignora il secondo campo
println!("x = {x}");
}

Output:

il punto è (13, 4)
a = 2, b = 1
x = 13
▶ Provalo tu

Lo scambio funziona perché Rust costruisce prima il lato destro, (b, a). Solo dopo lo spacchetta in a e b. Con _ e i pattern puoi fare molto di più: lo vedrai nella pagina sul Pattern matching.

In realtà hai già incontrato una tupla senza accorgertene. () si chiama tipo unit, ed è quello che restituisce una funzione quando non restituisce niente di significativo: è il tipo di ritorno implicito di fn main().

Le tuple sono ottime per raggruppamenti veloci e senza nome, soprattutto per restituire due o tre valori da una funzione. Quando un gruppo di valori rappresenta un concetto vero, con campi che hanno un nome (come una Person con nome ed età a cui farai riferimento per nome in tutto il codice), di solito una struct è la scelta migliore e più leggibile.

Scrivi una funzione sum_and_product(a: i32, b: i32) -> (i32, i32) che restituisce sia la somma sia il prodotto di due numeri. In main, chiamala con 5 e 6 e destruttura il risultato. Suggerimento: l’ultima riga della funzione è la tupla stessa, (a + b, a * b), senza punto e virgola.

Mostra la soluzione
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!("somma = {sum}, prodotto = {product}");
}

Output:

somma = 11, prodotto = 30
▶ Provalo tu

Un rettangolo si può salvare come una tupla di due punti, dove ogni punto è a sua volta una tupla: let rect = ((1, 2), (5, 5));. Calcola larghezza, altezza e area. Suggerimento: le tuple si possono annidare, e rect.1.0 significa «il primo campo del secondo punto».

Mostra la soluzione
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!("larghezza {width}, altezza {height}, area {}", width * height);
}

Output:

larghezza 4, altezza 3, area 12
▶ Provalo tu

Puoi anche destrutturare entrambi i punti in un colpo: let ((x1, y1), (x2, y2)) = rect;. È più lungo da scrivere, ma x2 - x1 si legge meglio di rect.1.0 - rect.0.0. Quando i punti cominciano ad accumularsi così, è il segnale che una struct con campi con un nome sarebbe più chiara.

  • Una tupla raggruppa valori anche di tipi diversi: (name, age, is_active).
  • Leggi i campi con .0, .1, .2, oppure tirali fuori tutti insieme con la destrutturazione.
  • Le tuple sono il modo naturale per restituire più valori da una funzione.
  • I campi di una tupla mut possono cambiare, ma la sua dimensione e i suoi tipi no. (a, b) = (b, a); scambia due valori.
  • Per raggruppamenti con un nome e riutilizzabili, usa invece una struct.

Verifica veloce

1. Che cosa può contenere una tupla che un array non può contenere?

2. Come leggi il secondo elemento di una tupla che si chiama person?

3. Dopo let mut a = 1; let mut b = 2; (a, b) = (b, a);, quanto valgono a e b?

Punteggio: 0 / 3