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

Memorizzare Chiavi con Valori Associati in Mappe Hash

L’ultima delle nostre collezioni comuni è la mappa hash. Il type HashMap<K, V> memorizza una mappatura di chiavi di type K a valori di type V utilizzando una funzione di hashing, che determina come queste chiavi e valori vengono inseriti in memoria. Molti linguaggi di programmazione supportano questo tipo di struttura dati, ma spesso usano un nome diverso, come hash, map, object, hash table, dictionary o associative array, solo per citarne alcuni.

Le mappe hash (hash map d’ora in poi) sono utili quando si desidera ricercare dati non utilizzando un indice, come è possibile con i vettori, ma utilizzando una chiave che può essere di qualsiasi type. Ad esempio, in una partita, è possibile tenere traccia del punteggio di ogni squadra in una hash map in cui ogni chiave è il nome di una squadra e i valori sono il punteggio di ogni squadra. Dato il nome di una squadra, è possibile recuperarne il punteggio.

In questa sezione esamineremo l’API di base delle hash map, ma molte altre funzionalità si nascondono nelle funzioni definite su HashMap<K, V> dalla libreria standard. Come sempre, consultate la documentazione della libreria standard per ulteriori informazioni.

Creare una Nuova Hash Map

Un modo per creare una hash map vuota è usare new e aggiungere elementi con insert. Nel Listato 8-20, stiamo tenendo traccia del punteggio di due squadre i cui nomi sono Blu e Gialla. La squadra Blu inizia con 10 punti, mentre la squadra Gialla inizia con 50.

fn main() {
    use std::collections::HashMap;

    let mut punteggi = HashMap::new();

    punteggi.insert(String::from("Blu"), 10);
    punteggi.insert(String::from("Gialla"), 50);
}
Listato 8-20: Creazione di una nuova hash map e inserimento di chiavi e valori

Nota che dobbiamo prima portare in scope HashMap con use dalla sezione dedicata alle collezioni della libreria standard. Delle nostre tre collezioni comuni, questa è la meno utilizzata, quindi non è inclusa nelle funzionalità aggiunte allo scope dal preludio. Le hash map hanno anche un supporto minore dalla libreria standard; ad esempio, non esiste una macro integrata per costruirle.

Proprio come i vettori, le hash map memorizzano i loro dati nell’heap. Questa HashMap ha chiavi di type String e valori di type i32. Come i vettori, le hash map sono omogenee: tutte le chiavi devono avere lo stesso type e tutti i valori devono avere lo stesso type.

Accedere ai Valori in una Hash Map

Possiamo ottenere un valore dalla hash map fornendo la sua chiave al metodo get, come mostrato nel Listato 8-21.

fn main() {
    use std::collections::HashMap;

    let mut punteggi = HashMap::new();

    punteggi.insert(String::from("Blu"), 10);
    punteggi.insert(String::from("Gialla"), 50);

    let nome_squadra = String::from("Blu");
    let punteggio = punteggi.get(&nome_squadra).copied().unwrap_or(0);
}
Listato 8-21: Accesso al punteggio per la squadra Blu memorizzato nella hash map

Qui, punteggio avrà il valore associato alla squadra Blu e il risultato sarà 10. Il metodo get restituisce Option<&V>; se non c’è alcun valore per quella chiave nella hash map, get restituirà None. Questo programma gestisce Option chiamando copied per ottenere Option<i32> anziché Option<&i32>, quindi unwrap_or per impostare punteggio a zero se punteggio non ha una voce per la chiave.

Possiamo iterare su ogni coppia chiave-valore in una hash map in modo simile a come facciamo con i vettori, utilizzando un ciclo for:

fn main() {
    use std::collections::HashMap;

    let mut punteggi = HashMap::new();

    punteggi.insert(String::from("Blu"), 10);
    punteggi.insert(String::from("Gialla"), 50);

    for (chiave, valore) in &punteggi {
        println!("{chiave}: {valore}");
    }
}

Questo codice stamperà ogni coppia in un ordine arbitrario:

Gialla: 50
Blu: 10

Gestire Ownership nelle Hash Map

Per i type che implementano il trait Copy, come i32, i valori vengono copiati nella hash map. Per i valori con ownership come String, i valori verranno spostati e la hash map assumerà la ownership di tali valori, come dimostrato nel Listato 8-22.

fn main() {
    use std::collections::HashMap;

    let nome_campo = String::from("Colore preferito");
    let valore_campo = String::from("Blu");

    let mut map = HashMap::new();
    map.insert(nome_campo, valore_campo);
    // `nome_campo` e `valore_campo` non sono validi a questo punto,
    // prova a usarli e vedi quale errore di compilazione ottieni!
}
Listato 8-22: Mostra che chiavi e valori sono di proprietà della hash map una volta inseriti

Non possiamo utilizzare le variabili nome_campo e valore_campo dopo che sono state spostate nella hash map con la chiamata a insert.

Se inseriamo reference a valori nella hash map, i valori non verranno spostati nella hash map. I valori a cui puntano i reference devono essere validi almeno per il tempo in cui è valida la hash map. Approfondiremo questi argomenti in “Validare i Reference con la Lifetime nel Capitolo 10.

Aggiornare una Hash Map

Sebbene il numero di coppie chiave-valore sia espandibile, a ogni chiave univoca può essere associato un solo valore alla volta (ma non viceversa: ad esempio, sia la squadra Blu che quella Gialla potrebbero avere il valore 10 memorizzato nella hash map punteggi).

Quando si desidera modificare i dati in una hash map, è necessario decidere come gestire il caso in cui a una chiave sia già assegnato un valore. È possibile sostituire il vecchio valore con il nuovo valore, ignorando completamente il vecchio valore. È possibile mantenere il vecchio valore e ignorare il nuovo valore, aggiungendo il nuovo valore solo se la chiave non ha già un valore. Oppure è possibile combinare il vecchio valore e il nuovo valore. Vediamo come fare ciascuna di queste cose!

Sovrascrivere un Valore

Se inseriamo una chiave e un valore in una hash map e poi inseriamo la stessa chiave con un valore diverso, il valore associato a quella chiave verrà sostituito. Anche se il codice nel Listato 8-23 chiama insert due volte, la hash map conterrà solo una coppia chiave-valore perché stiamo inserendo il valore per la chiave della squadra Blu entrambe le volte.

fn main() {
    use std::collections::HashMap;

    let mut punteggi = HashMap::new();

    punteggi.insert(String::from("Blu"), 10);
    punteggi.insert(String::from("Blu"), 25);

    println!("{punteggi:?}");
}
Listato 8-23: Sostituzione di un valore memorizzato con una chiave specifica

Questo codice stamperà {"Blu": 25}. Il valore originale di 10 è stato sovrascritto.

Aggiungere una Chiave e un Valore Solo Se una Chiave Non è Presente

È comune verificare se una particolare chiave esiste già nella hash map con un valore e quindi eseguire le seguenti azioni: se la chiave esiste nella hash map, il valore esistente deve rimanere invariato; se la chiave non esiste, inserirla e assegnarle un valore.

Le hash map dispongono di un’API speciale per questo scopo, chiamata entry, che accetta la chiave che si desidera controllare come parametro. Il valore restituito dal metodo entry è un’enum chiamato Entry che rappresenta un valore che potrebbe esistere o meno. Supponiamo di voler verificare se la chiave per la squadra Gialla ha un valore associato. In caso contrario, vogliamo inserire il valore 50, e lo stesso vale per la squadra Blu. Utilizzando l’API entry, il codice appare come nel Listato 8-24.

fn main() {
    use std::collections::HashMap;

    let mut punteggi = HashMap::new();
    punteggi.insert(String::from("Blu"), 10);

    punteggi.entry(String::from("Gialla")).or_insert(50);
    punteggi.entry(String::from("Blu")).or_insert(50);

    println!("{punteggi:?}");
}
Listato 8-24: Utilizzo del metodo entry per inserire solo se la chiave non ha già un valore

Il metodo or_insert su Entry è definito per restituire un reference mutabile al valore della chiave Entry corrispondente se tale chiave esiste, e in caso contrario, inserisce il parametro come nuovo valore per questa chiave e restituisce un reference mutabile al nuovo valore. Questa tecnica è molto più pulita rispetto alla scrittura manuale della logica e, inoltre, si integra meglio con il borrow checker.

L’esecuzione del codice nel Listato 8-24 stamperà {"Gialla": 50, "Blu": 10}. La prima chiamata a entry inserirà la chiave per la squadra Gialla con il valore 50 perché la squadra Gialla non ha già un valore. La seconda chiamata a entry non modificherà la hash map perché la squadra Blu ha già il valore 10.

Aggiornare un Valore in Base al Valore Precedente

Un altro caso d’uso comune per le hash map è cercare il valore di una chiave e quindi aggiornarlo in base al valore precedente. Ad esempio, il Listato 8-25 mostra un codice che conta quante volte ogni parola appare in un testo. Utilizziamo una hash map con le parole come chiavi e incrementiamo il valore per tenere traccia di quante volte abbiamo visto quella parola. Quando incontriamo una parola nuova, verrà inserita inizializzando il valore associato a 0.

fn main() {
    use std::collections::HashMap;

    let testo = "hello world wonderful world";

    let mut map = HashMap::new();

    for parola in testo.split_whitespace() {
        let conteggio = map.entry(parola).or_insert(0);
        *conteggio += 1;
    }

    println!("{map:?}");
}
Listato 8-25: Conteggio delle occorrenze di parole utilizzando una hash map che memorizza parole e conteggi

Questo codice stamperà {"world": 2, "hello": 1, "wonderful": 1}. Potresti vedere le stesse coppie chiave-valore stampate in un ordine diverso: ricorda che, come menzionato precedentemente in “Accesso ai Valori in una Hash Map, l’iterazione su una hash map avviene in un ordine arbitrario.

Il metodo split_whitespace restituisce un iteratore su slice, separate da spazi, del valore in testo. Il metodo or_insert restituisce un reference mutabile (&mut V) al valore della chiave specificata. Qui, memorizziamo quel reference mutabile nella variabile conteggio, quindi per assegnare quel valore, dobbiamo prima de-referenziare conteggio usando l’asterisco (*). Il reference mutabile esce dallo scope alla fine del ciclo for, quindi tutte queste modifiche sono sicure e consentite dalle regole di prestito.

Funzioni di Hashing

Come impostazione predefinita, HashMap utilizza una funzione di hashing chiamata SipHash che può fornire resistenza agli attacchi denial-of-service (DoS) che coinvolgono tabelle di hash1. Questo non è l’algoritmo di hashing più veloce disponibile, ma è un buon compromesso in termini di maggiore sicurezza che ne deriva, nonostante il costo prestazionale derivato. Se si profila il codice e si scopre che la funzione di hashing predefinita è troppo lenta per i propri scopi, è possibile passare a un’altra funzione specificando un hasher diverso. Un hasher è un type che implementa il trait BuildHasher. Parleremo dei trait e di come implementarli nel Capitolo 10. Non è necessario implementare il proprio hasher da zero; crates.io offre librerie condivise da altri utenti Rust che forniscono hasher che implementano molti algoritmi di hashing comuni.

Vettori, stringhe e hash map forniranno una grande quantità di funzionalità necessarie nei programmi quando avrai necessità di memorizzare, accedere e modificare dati. Ecco alcuni esercizi che ora dovresti essere in grado di risolvere:

  1. Dato un elenco di interi, usa un vettore e restituisci la mediana (quando ordinati, il valore in posizione centrale) e la moda (il valore che ricorre più spesso; una hash map sarà utile in questo caso) dell’elenco.
  2. Converti delle stringhe in pig latin. La prima consonante di ogni parola viene spostata alla fine della parola e viene aggiunto ay, quindi primo diventa rimo-pay. Le parole che iniziano con una vocale hanno invece hay aggiunto alla fine (ananas diventa ananas-hay). Tieni a mente i dettagli sulla codifica UTF-8!
  3. Utilizzando hash map e vettori, crea un’interfaccia testuale che consenta a un utente di aggiungere i nomi dei dipendenti a un reparto di un’azienda; ad esempio, “Aggiungi Sally a Ingegneria” o “Aggiungi Amir a Vendite”. Quindi, consenti all’utente di recuperare un elenco di tutte le persone in un reparto o di tutte le persone in azienda per reparto, ordinate alfabeticamente.

La documentazione API della libreria standard descrive i metodi di vettori, stringhe e hash map che saranno utili per questi esercizi!

Stiamo entrando in programmi più complessi in cui le operazioni possono fallire, quindi è il momento perfetto per discutere della gestione degli errori. Lo faremo in seguito!


  1. SipHash su wikipedia (eng)