Salta ai contenuti

Generici in Rust

Hai già usato i generici (generics) anche senza chiamarli per nome: Vec<i32> e Option<T> li usano entrambi. Questa pagina spiega come scrivere funzioni e struct generiche tue.

Mettiamo che tu voglia una funzione che restituisce il più grande di una lista di numeri, sia per i32 sia per f64. Senza generici, dovresti scriverla due volte:

fn largest_i32(numbers: &[i32]) -> i32 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn largest_f64(numbers: &[f64]) -> f64 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}

Stessa logica, copiata e incollata per ogni tipo. I generici ti permettono di scriverla una volta sola.

fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn main() {
let integers = [10, 25, 3, 47, 8];
let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers));
println!("{}", largest(&floats));
}

Output:

47
2.8
▶ Provalo tu

<T: PartialOrd + Copy> si legge «per qualsiasi tipo T, purché si possa confrontare (PartialOrd) e copiare (Copy)». T è un segnaposto: Rust genera una versione della funzione per ogni tipo concreto con cui la chiami davvero (i32, f64 e così via).

Anche le struct possono essere generiche:

struct Pair<T> {
first: T,
second: T,
}
fn main() {
let numbers = Pair { first: 5, second: 10 };
let words = Pair { first: "ciao", second: "mondo" };
println!("{} {}", numbers.first, numbers.second);
println!("{} {}", words.first, words.second);
}

Output:

5 10
ciao mondo
▶ Provalo tu

Una sola definizione di Pair<T> funziona per numeri, testo o qualsiasi altro tipo: numbers è un Pair<i32> e words è un Pair<&str>, ma la struct l’hai scritta una volta sola.

No. Rust genera una versione separata e specializzata della funzione o della struct per ogni tipo concreto che usi davvero (si chiama monomorfizzazione, in inglese monomorphization). Quando il programma gira non resta più niente di generico: solo funzioni normali con tipi precisi, veloci esattamente come se le avessi scritte una per una a mano.

Puoi aggiungere metodi a una struct generica con un blocco impl, proprio come nella pagina sulle struct. L’unica novità è che anche l’impl ha bisogno del suo <T>:

struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn swap(self) -> Pair<T> {
Pair { first: self.second, second: self.first }
}
}
impl Pair<f64> {
fn average(&self) -> f64 {
(self.first + self.second) / 2.0
}
}
fn main() {
let words = Pair::new("sinistra", "destra").swap();
println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0);
println!("{}", scores.average());
}

Output:

destra sinistra
8
▶ Provalo tu

Qui ci sono due tipi di blocco impl:

  • impl<T> Pair<T> vuol dire «questi metodi esistono per un Pair di qualsiasi tipo». Leggi il primo <T> come «per qualsiasi tipo T». Senza, Rust penserebbe che T sia il nome di un tipo vero e si lamenterebbe di non trovarlo.
  • impl Pair<f64> non ha <T>. I suoi metodi esistono solo per Pair<f64>. Ecco perché average può usare + e / 2.0. Prova a chiamare words.average(): il compilatore ti dirà che per Pair<&str> quel metodo non esiste.

Una struct può anche avere più di un parametro di tipo, come struct Pair<T, U> { first: T, second: U }, quando i due campi possono avere tipi diversi. E quando i vincoli di trait diventano lunghi, puoi spostarli in una clausola where per mantenere leggibile la firma. Ne parliamo nella pagina sui trait avanzati.

Scrivi fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T, che restituisce l’elemento più piccolo. Provala con [8, 3, 5] e con i caratteri ['r', 'u', 's', 't']. Suggerimento: è la funzione largest di questa pagina con un confronto invertito.

Mostra la soluzione
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut smallest = items[0];
for &item in items {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
println!("{}", smallest(&[8, 3, 5]));
println!("{}", smallest(&['r', 'u', 's', 't']));
}

Output:

3
r
▶ Provalo tu

Aggiungi a Pair<T> un metodo larger(&self) -> &T che restituisce un riferimento al campo più grande. Provalo con un Pair di numeri e con un Pair di parole. Suggerimento: per confrontare serve PartialOrd, quindi metti il vincolo direttamente sull’impl: impl<T: PartialOrd> Pair<T>.

Mostra la soluzione
struct Pair<T> {
first: T,
second: T,
}
impl<T: PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first > self.second {
&self.first
} else {
&self.second
}
}
}
fn main() {
let numbers = Pair { first: 3, second: 9 };
let words = Pair { first: "pera", second: "mela" };
println!("{}", numbers.larger());
println!("{}", words.larger());
}

Output:

9
pera
▶ Provalo tu

Le stringhe si confrontano lettera per lettera, come in un dizionario: "pera" viene dopo "mela", quindi è la «più grande».

  • I generici ti permettono di scrivere una sola funzione o struct che funziona con più tipi, invece di duplicare il codice.
  • <T> è un tipo segnaposto; i vincoli di trait (T: PartialOrd) limitano cosa può essere T.
  • impl<T> Pair<T> aggiunge metodi per ogni T; impl Pair<f64> aggiunge metodi solo per un tipo concreto.
  • Il codice generico viene compilato in versioni specializzate per ogni tipo: nessun costo in prestazioni a runtime.

Verifica veloce

1. Quale problema risolvono i generici?

2. In fn largest<T: PartialOrd + Copy>(...), cosa fa PartialOrd + Copy?

3. Usare i generici costa qualcosa a runtime rispetto al codice scritto a mano?

4. Cosa ti permette di fare una sola definizione di struct Pair<T>?

Punteggio: 0 / 4