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.
Il problema che i generici risolvono
Sezione intitolata “Il problema che i generici risolvono”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.
Una funzione generica
Sezione intitolata “Una funzione generica”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:
472.8<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).
Una struct generica
Sezione intitolata “Una struct generica”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 10ciao mondoUna 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.
Costa qualcosa a runtime?
Sezione intitolata “Costa qualcosa a runtime?”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.
Metodi su una struct generica
Sezione intitolata “Metodi su una struct generica”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 sinistra8Qui ci sono due tipi di blocco impl:
impl<T> Pair<T>vuol dire «questi metodi esistono per unPairdi qualsiasi tipo». Leggi il primo<T>come «per qualsiasi tipoT». Senza, Rust penserebbe cheTsia il nome di un tipo vero e si lamenterebbe di non trovarlo.impl Pair<f64>non ha<T>. I suoi metodi esistono solo perPair<f64>. Ecco perchéaveragepuò usare+e/ 2.0. Prova a chiamarewords.average(): il compilatore ti dirà che perPair<&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.
Pratica
Sezione intitolata “Pratica”1. Il più piccolo, in versione generica
Sezione intitolata “1. Il più piccolo, in versione generica”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:
3r2. La metà più grande di un Pair
Sezione intitolata “2. La metà più grande di un Pair”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:
9peraLe stringhe si confrontano lettera per lettera, come in un dizionario: "pera" viene dopo "mela", quindi è la «più grande».
Riepilogo
Sezione intitolata “Riepilogo”- 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ò essereT.impl<T> Pair<T>aggiunge metodi per ogniT;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
Sezione intitolata “Verifica veloce”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
Per approfondire
Sezione intitolata “Per approfondire”- Generic Data Types — The Rust Book (in inglese): i generici in funzioni, struct, enum e metodi, compreso l’esempio
largest. - Generics — Rust by Example (in inglese): brevi esempi eseguibili di funzioni e struct generiche e della loro sintassi.
- Implementation — Rust by Example (in inglese): di più sui blocchi
impl<T>e su come implementare metodi per un solo tipo concreto. - Bounds — Rust by Example (in inglese): come i vincoli di trait limitano cosa può essere un tipo generico, e perché ti servono.
- Generic parameters — The Rust Reference (in inglese): le regole precise, per quando vuoi tutti i dettagli.