SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Definire un Enum
Le struct ti permettono di raggruppare campi e dati correlati, come un Rettangolo con la sua larghezza e altezza, mentre gli enum ti permettono di dire che un valore è uno dei possibili valori di un insieme. Ad esempio, potremmo voler dire che Rettangolo è uno dei possibili tipi di forme che include anche Cerchio e Triangolo. Per fare ciò, Rust ci permette di codificare queste possibilità come un enum.
Immaginiamo una situazione che potremmo voler esprimere nel codice e vediamo perché gli enum sono utili e più appropriati delle struct in questo caso. Diciamo che dobbiamo lavorare con gli indirizzi IP. Attualmente, vengono utilizzati due standard principali per gli indirizzi IP: la versione quattro e la versione sei. Poiché queste sono le uniche possibilità per un indirizzo IP che il nostro programma incontrerà, possiamo elencare tutte le varianti possibili, da qui il nome di enumerazione.
Qualsiasi indirizzo IP può essere sia un indirizzo versione quattro che un indirizzo versione sei, ma non entrambi contemporaneamente. Questa proprietà degli indirizzi IP rende appropriata la struttura dati enum perché un valore enum può essere solo una delle sue varianti. Entrambi gli indirizzi versione quattro e versione sei sono ancora fondamentalmente indirizzi IP, quindi dovrebbero essere trattati come lo stesso tipo quando il codice gestisce situazioni che si applicano a qualsiasi tipo di indirizzo IP.
Possiamo esprimere questo concetto nel codice definendo un’enumerazione IpAddrKind e elencando i possibili tipi di indirizzo IP, V4 e V6. Queste sono le varianti dell’enum:
rust
enum IpAddrKind {
V4,
V6,
}
IpAddrKind è ora un tipo di dato personalizzato che possiamo utilizzare altrove nel nostro codice.
Valori Enum
Possiamo creare istanze di ciascuna delle due varianti di IpAddrKind in questo modo:
rust
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
Nota che le varianti dell’enum sono racchiuse nello spazio dei nomi sotto il suo identificatore, e usiamo due punti doppi per separarle. Questo è utile perché ora entrambi i valori IpAddrKind::V4 e IpAddrKind::V6 sono dello stesso tipo: IpAddrKind. Possiamo quindi, per esempio, definire una funzione che accetta qualsiasi IpAddrKind:
rust
fn route(ip_kind: IpAddrKind) {}
E possiamo chiamare questa funzione con qualsiasi variante:
rust
route(IpAddrKind::V4);
route(IpAddrKind::V6);
L’utilizzo degli enum ha anche altri vantaggi. Pensando di più al nostro tipo di indirizzo IP, al momento non abbiamo un modo per memorizzare i dati effettivi dell’indirizzo IP; sappiamo solo di che tipo è. Dato che hai appena imparato le struct nel Capitolo 5, potresti essere tentato di affrontare questo problema con le struct come mostrato nel Listato 6-1.
rust
enum IpAddrKind {
V4,
V6,
} struct IpAddr {
kind: IpAddrKind,
address: String,
} let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
Listato 6-1: Memorizzazione dei dati e variante IpAddrKind di un indirizzo IP usando una struct
Qui abbiamo definito una struct IpAddr che ha due campi: un campo kind di tipo IpAddrKind (l’enum che abbiamo definito in precedenza) e un campo address di tipo String. Abbiamo due istanze di questa struct. Il primo è home, e ha il valore IpAddrKind::V4 come suo tipo con dati di indirizzo associati 127.0.0.1. La seconda istanza è loopback. Ha l’altra variante di IpAddrKind come suo valore di tipo, V6, e ha associato l’indirizzo ::1 ad esso. Abbiamo usato una struct per raggruppare i valori kind e address insieme, quindi ora la variante è associata al valore.
Tuttavia, rappresentare lo stesso concetto utilizzando solo un enum è più conciso: anziché un enum all’interno di una struct, possiamo inserire i dati direttamente in ciascuna variante dell’enum. Questa nuova definizione dell’enum IpAddr dice che entrambe le varianti V4 e V6 avranno valori String associati:
rust
enum IpAddr {
V4(String),
V6(String),
} let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
Allegriamo dati a ciascuna variante dell’enum direttamente, quindi non c’è bisogno di una struct aggiuntiva. Qui, è anche più facile vedere un altro dettaglio su come funzionano gli enum: il nome di ciascuna variante dell’enum che definiamo diventa anche una funzione che costruisce un’istanza dell’enum. Cioè, IpAddr::V4() è una chiamata di funzione che prende un argomento di tipo String e restituisce un’istanza del tipo IpAddr. Otteniamo automaticamente questa funzione costruttrice definita come risultato della definizione dell’enum.
C’è un altro vantaggio nell’usare un enum piuttosto che una struct: ciascuna variante può avere tipi di dati associati diversi e quantità di dati associati. Gli indirizzi IP della versione quattro avranno sempre quattro componenti numerici che avranno valori compresi tra 0 e 255. Se volessimo memorizzare gli indirizzi V4 come quattro valori u8 ma esprimere comunque gli indirizzi V6 come un valore Stringa, non saremmo in grado di farlo con una struct. Gli enum gestiscono questo caso con facilità:
rust
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
} let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
Abbiamo mostrato diversi modi di definire strutture dati per memorizzare gli indirizzi IP della versione quattro e sei. Tuttavia, come si scopre, il desiderio di memorizzare gli indirizzi IP e codificare il loro tipo è così comune che la libreria standard ha una definizione che possiamo usare! Vediamo come la libreria standard definisce IpAddr: ha l’enum esatto e le varianti che abbiamo definito e usato, ma incorpora i dati dell’indirizzo all’interno delle varianti sotto forma di due diverse strutture dati, che sono definite diversamente per ciascuna variante:
rust
struct Ipv4Addr {
// --snip--
} struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
Questo codice illustra che è possibile inserire qualsiasi tipo di dati all’interno di una variante di enum: stringhe, tipi numerici o strutture dati, ad esempio. È possibile includere persino un altro enum! Inoltre, i tipi di libreria standard non sono spesso molto più complicati di quanto potresti pensare.
Nota che anche se la libreria standard contiene una definizione per IpAddr, possiamo comunque creare e utilizzare la nostra definizione senza conflitti perché non abbiamo portato la definizione della libreria standard nel nostro ambito. Parleremo di più sull’introduzione di tipi nell’ambito nel Capitolo 7.
Guardiamo un altro esempio di un enum nella libreria standard che è molto comune e utile: Option.