Type Generici, Trait e Lifetime
Ogni linguaggio di programmazione dispone di strumenti per gestire efficacemente la duplicazione di concetti. In Rust, uno di questi strumenti sono i type generici: sostituti astratti per type concreti o altre proprietà. Possiamo esprimere il comportamento dei type generici o come si relazionano ad altri type generici senza sapere cosa ci sarà al loro posto durante la compilazione e l’esecuzione del codice.
Le funzioni possono accettare parametri di un type generico, invece di un
type concreto come i32
o String
, allo stesso modo in cui accettano
parametri con valori sconosciuti per eseguire lo stesso codice su più valori
concreti. Infatti, abbiamo già utilizzato i generici nel Capitolo 6 con
Option<T>
, nel Capitolo 8 con Vec<T>
e HashMap<K, V>
e nel Capitolo 9 con
Result<T, E>
. In questo capitolo, esplorerai come definire i tuoi type,
funzioni e metodi personalizzati con i generici!
Per prima cosa, esamineremo come estrarre una funzione per ridurre la duplicazione del codice. Utilizzeremo quindi la stessa tecnica per creare una funzione generica da due funzioni che differiscono solo per il type dei loro parametri. Spiegheremo anche come utilizzare i type generici nelle definizioni di struct ed enum.
Poi imparerai come utilizzare i trait per definire il comportamento in modo generico. Puoi combinare i trait con i type generici per vincolare un type generico ad accettare solo i type che hanno un comportamento particolare, anziché qualsiasi type.
Infine, parleremo della longevità (lifetime): una varietà di generici che fornisce al compilatore informazioni su come i reference si relazionano tra loro. I lifetime ci permettono di fornire al compilatore informazioni sufficienti sui valori presi in prestito in modo che possa garantire che i reference siano validi in più situazioni di quante ne potrebbe avere senza il nostro aiuto.
Rimuovere la Duplicazione Mediante l’Estrazione di una Funzione
I type generici ci permettono di sostituire type specifici con un segnaposto che rappresenta più type per evitare la duplicazione del codice. Prima di addentrarci nella sintassi dei type generici, vediamo come rimuovere le ripetizioni in un modo che non coinvolga type generici, estraendo una funzione che sostituisce valori specifici con un segnaposto che rappresenta più valori. Poi applicheremo la stessa tecnica per estrarre una funzione generica! Analizzando come riconoscere il codice duplicato che è possibile estrarre in una funzione, comincerai a riconoscere il codice duplicato che può utilizzare i generici.
Inizieremo con il breve programma nel Listato 10-1 che trova il numero più grande in un elenco.
fn main() { let lista_numeri = vec![34, 50, 25, 100, 65]; let mut maggiore = &lista_numeri[0]; for numero in &lista_numeri { if numero > maggiore { maggiore = numero; } } println!("Il numero maggiore è {maggiore}"); assert_eq!(*maggiore, 100); }
Memorizziamo un elenco di numeri interi nella variabile lista_numeri
e
inseriamo un reference al primo numero dell’elenco in una variabile denominata
maggiore
. Quindi eseguiamo un’iterazione su tutti i numeri dell’elenco e, se
il numero corrente è più grande del numero memorizzato in maggiore
,
sostituiamo il reference in quella variabile. Tuttavia, se il numero corrente
è minore o uguale al numero più grande visto finora, la variabile non cambia e
il codice passa al numero successivo nell’elenco. Dopo aver considerato tutti i
numeri nell’elenco, maggiore
dovrebbe riferirsi al numero più grande, che in
questo caso è 100.
Ora ci è stato chiesto di trovare il numero più grande in due diversi elenchi di numeri. Per farlo, possiamo scegliere di duplicare il codice nel Listato 10-1 e utilizzare la stessa logica in due punti diversi del programma, come mostrato nel Listato 10-2.
fn main() { let lista_numeri = vec![34, 50, 25, 100, 65]; let mut maggiore = &lista_numeri[0]; for numero in &lista_numeri { if numero > maggiore { maggiore = numero; } } println!("Il numero maggiore è {maggiore}"); let lista_numeri = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut maggiore = &lista_numeri[0]; for numero in &lista_numeri { if numero > maggiore { maggiore = numero; } } println!("Il numero maggiore è {maggiore}"); }
Sebbene questo codice funzioni, duplicarlo è noioso e soggetto a errori. Dobbiamo anche ricordarci di aggiornare il codice in più punti quando vogliamo modificarlo.
Per eliminare questa duplicazione, creeremo un’astrazione definendo una funzione che opera su qualsiasi elenco di integer passati come parametro. Questa soluzione rende il nostro codice più chiaro e ci permette di esprimere il concetto di ricerca del numero più grande in un elenco in modo astratto.
Nel Listato 10-3, estraiamo il codice che trova il numero più grande in una
funzione denominata maggiore
. Quindi chiamiamo la funzione per trovare il
numero più grande nelle due liste del Listato 10-2. Potremmo anche utilizzare la
funzione su qualsiasi altro elenco di valori i32
che potremmo avere in futuro.
fn maggiore(lista: &[i32]) -> &i32 { let mut maggiore = &list[0]; for elemento in lista { if elemento > maggiore { maggiore = elemento; } } maggiore } fn main() { let lista_numeri = vec![34, 50, 25, 100, 65]; let risultato = maggiore(&lista_numeri); println!("Il numero maggiore è {risultato}"); assert_eq!(*risultato, 100); let lista_numeri = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let risultato = maggiore(&lista_numeri); println!("Il numero maggiore è {risultato}"); assert_eq!(*risultato, 6000); }
La funzione maggiore
ha un parametro chiamato lista
, che rappresenta
qualsiasi slice concreta di valori i32
che potremmo passare alla funzione.
Di conseguenza, quando chiamiamo la funzione, il codice viene eseguito sui
valori specifici che passiamo.
In sintesi, ecco i passaggi che abbiamo seguito per modificare il codice dal Listato 10-2 al Listato 10-3
- Identificare il codice duplicato.
- Estrarre il codice duplicato nel corpo della funzione e specificare gli input e i valori restituiti da tale codice nella firma della funzione.
- Aggiornare le due istanze di codice duplicato per chiamare la funzione.
Successivamente, utilizzeremo gli stessi passaggi con i type generici per
ridurre la duplicazione del codice. Allo stesso modo in cui il corpo della
funzione può operare su una lista
astratta anziché su valori specifici, i
type generici consentono al codice di operare su type astratti.
Ad esempio, supponiamo di avere due funzioni: una che trova l’elemento più
grande in un insieme di valori i32
e una che trova l’elemento più grande in un
insieme di valori char
. Come elimineremmo questa duplicazione? Scopriamolo!