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); }
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); }
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! }
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:?}"); }
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:?}"); }
entry
per inserire solo se la chiave non ha già un valoreIl 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:?}"); }
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.
Riepilogo
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:
- 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.
- 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!
- 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!