Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

File: src/main.rs
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);
}
Listato 10-1: Trovare il numero più grande in un elenco di numeri

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.

File: src/main.rs
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}");
}
Listato 10-2: Codice per trovare il numero più grande in due elenchi di numeri

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.

File: src/main.rs
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);
}
Listato 10-3: Codice astratto per trovare il numero più grande in due liste

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

  1. Identificare il codice duplicato.
  2. Estrarre il codice duplicato nel corpo della funzione e specificare gli input e i valori restituiti da tale codice nella firma della funzione.
  3. 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!