Le strutture dati sono necessarie in qualsiasi forma di programmazione, compresa quella in Rust. Dopo le variabili costituiscono il modo per poter organizzare le informazioni sia internamente sia tra di loro, permettendo di costruire delle vere e proprie reti da analizzare. Ne abbiamo già incontrate tre fondamentali in questa guida: array, tuple e vettori.
Queste rispondono perfettamente ad alcune esigenze ma per poter realizzare un programma completo ce ne servono altre. Le strutture dati non sono fatte solo di dati ma anche di politiche di gestione ovvero le modalità in cui i dati possono al loro interno essere letti e scritti. In base a queste, sapremo quale sarà la struttura dati giusta da impiegare in un determinato contesto.
Politiche di gestione in Rust
Le politiche di gestione principali sono tre:
- accesso sequenziale: i dati vengono gestiti in base alla posizione che permette di distinguerli gli uni dagli altri (sono pertanto ammessi i duplicati).
Questa esigenza è soddisfatta dalle strutture già citate ovvero array, vettori e tuple; - accesso chiave/valore: ogni dato non viene inserito da solo ma accompagnato da una chiave che serve per recuperarlo istantaneamente;
- valori singoli senza duplicati: è il caso dei
set
.
In questa lezione ci occuperemo di imparare ulteriori strutture dati oltre a quelle già viste in modo da avere un ventaglio di scelte completo per affrontare ogni casistica che si possa presentare nei nostri programmi. In particolare, ci concentreremo sul secondo e terzo approccio dell'elenco precedente: chiave/valore e set.
HashMap: gestione dei dati chiave/valore
Il modo più immediato per lavorare con l'accesso chiave valore consiste nell'uso di una HashMap
importabile con:
use std::collections::HashMap;
accedendo alle collections della libreria standard.
Come primo esperimento si può iniziare a creare una HashMap
ed inserirvi alcuni elementi tenendo ben presente che andremo ad immettere sempre una coppia ovvero un valore accompagnato da una chiave. Qui supporremo di registrare allievi con i loro voti: la chiave sarà una stringa contenente cognome e nome dell'allievo ed il valore un numero in virgola per rappresentare il voto:
let mut allievi: HashMap<String, f32> = HashMap::new();
allievi.insert(String::from("Rossi Enzo"), 6.5);
allievi.insert(String::from("Bianchi Ilenia"), 8.0);
println!("HashMap = {:?}", allievi);
Abbiamo creato una struttura mutabile e con il metodo insert
vi abbiamo inserito una coppia alla volta. L'output che si ottiene è il seguente:
HashMap = {"Rossi Enzo": 6.5, "Bianchi Ilenia": 8.0}
L'operazione di lettura richiederà la presentazione di una chiave per ottenere, in risposta, il valore corrispondente. Ad esempio, che voto ha preso Ilenia? Chiediamolo alla HashMap:
println!("Voti di Ilenia = {:?}", allievi.get("Bianchi Ilenia"));
Avremo come risposta:
Voti di Ilenia = Some(8.0)
Notiamo che nella stampa "diretta" che si è ottenuta è stato presentato il valore come Some
in quanto non vi è certezza della presenza della chiave richiesta. In alternativa, in caso di chiave non esistente, avremmo ottenuto - come abbiamo già imparato - un None
.
Altre funzioni per la manipolazione in Rust
Esistono inoltre altre funzioni per la manipolazione di questi costrutti che avremo modo di sperimentare nelle prossime esercitazioni. Tra questi:
remove
per la rimozione di un elemento in base alla chiave;len
per conoscere il numero di accoppiate inserite;keys
per ottenere l'elenco delle chiavi;values
per avere un elenco dei valori.
Set: collezioni senza duplicati
Spesso - non stiamo assolutamente parlando di un caso di nicchia - è fondamentale collezionare elementi senza duplicati. Se ad esempio stiamo cercando URL di pagine web e vogliamo inserirle in maniera unica all'interno di una struttura dati, con i vettori dovremmo, ad ogni inserimento, verificare che l'elemento non sia già presente. In tali casi, avere una struttura dati che non ammette duplicati è fondamentale perché, alla seconda immissione del medesimo valore, questo verrebbe ignorato senza bisogno di alcun controllo da parte nostra con un bel ritorno in termini di produttività ed efficienza. Impariamo ad usare questi costrutti in Rust con l'HashSet
:
use std::collections::HashSet;
fn main() {
let mut frutti: HashSet<&str> = HashSet::new();
frutti.insert("banana");
frutti.insert("mela");
frutti.insert("pera");
frutti.insert("mela");
frutti.insert("pesca");
frutti.insert("banana");
frutti.insert("mela");
println!("Frutti presenti = {:?}", frutti);
if frutti.contains("mela") {
println!("Abbiamo la mela")
}
else {
println!("La mela manca")
}
}
L'esempio ricalca quello precedente sulle "mappe" prevedendo la creazione di un HashSet
ed inserendovi degli elementi. Come si vede, abbiamo provato ad inserire più termini ripetuti (mela e banana) che sono però stati accettati una sola volta infatti l'output del primo println
equivale a Frutti presenti = {"banana", "pera", "mela", "pesca"}. Il blocco if
che segue mostra come poter verificare che un valore sia presente: in questo caso, la mela c'è!
Anche i set sono forniti di molti metodi che possono essere sfruttati per le varie operazioni. Al momento, possiamo citare come principali strumenti utili:
remove
per la rimozione di elementi;union
per l'operazione insiemistica di unione di insiemi;intersection
per l'operazione insiemistica di intersezione di insiemi;difference
per l'operazione insiemistica di differenza di insiemi.
Anche in questo caso rimandiamo ulteriori approfondimenti ai prossimi esercizi che faremo insieme.