Rust - Gli enumeratori
Altro interessante argomento relativo ai tipi definiti dall'utente è
quello degli enumeratori. Si tratta di un argomento decisamente importante
tanto che il linguaggio, come vedremo, prevede, ad esempio, un paio di
enumeratori fondamentali ovvero Option e
Result, ma in generale essi sono ben
presenti. In pratica, permettono di definire un tipo che può
assumere una tra diversi possibili varianti. Ogni variante,
che è proprio il nome di ciascuno degli elementi interni, dell'enumeratore,
può essere semplice, senza dati, oppure può portare con sé ulteriori dati.
In parole povere, è un tipo definito dal programmatore che permette
di selezionare un valore da un elenco.
Diamo la solita
definizione formale che si basa sulla keyword enum:
enum Identificatore {
Variante-1,
Variante-2,
.....,
Variante-n,
}
gli identificatori sia per l'enumeratore che per le varianti, che sono separate tra di loro dalla virgola, dovrebbero seguire le consute regole CamelCase. I tipi abbinati alle singole varianti possono essere diversi tra loro, questo è importante, in conseguenza di ciò la varietà di situazioni che si presentano quando definite un enumeratore possono essere davvero tante. La definizione di un enumeratore tuttavia può essere più complessa in quanto ogni variante può accettare:
- specifica del tipo (uno o più) tra parentesi tonde
- un nome (o più) seguito dai due punti e dalla specifica del relativo tipo tra parentesi graffe.
vediamo un possibile esempio riassuntivo:
enum Mouse {
Assente,
Peso(u8),
Posizione{x: i32, y: i32},
Colore(String),
}
quindi abbiamo un primo campo che presenta solo un identificatore, un secondo che propone un tipo, un terzo che inserisce anche un nome per il tipo proposto e un quarto che presenta un altro tipo. Come si vede c'è varietà assoluta e questo è uno dei punti di forza degli enumeratori. Come le struct anche gli enumeratori sono sostnzialmente dei tipi creati dal programmatore e quindi devono essere utilizzati in qualche modo. Vediamo un esempio assolutamente banale che però fa uso del pattern matching, ahimè con gli enumeratori è quasi impossibile farne a meno (anzi sono i compagni di viaggio perfetti) se li si vogliono utilizzare appieno, comunque anche se non abbiamo ancora parlato di pattern matching direi che si può già intuire il funzionamento:
Esempio 16.1
enum Saluto {
Arrivo(String),
Partenza(String),
}
fn main() {
let messaggio = Saluto::Arrivo("Ciao!".to_string());
match messaggio {
Saluto::Arrivo(s) => println!("Messaggio di arrivo: {}", s),
Saluto::Partenza(s) => println!("Messaggio di partenza: {}", s),
}
}
Fa la sua comparsa l'operatore :: (double
colon) che ci permette di accedere alle varianti di un enumeratore.
Un altro esempio, classico, lo troverete molto simile su parecchi testi
e siti (vi prego di ignorare i numerosi warning):
Esempio 16.2
#[derive(Debug)]
enum Colore {
Rosso,
Giallo,
Verde,
}
fn main(){
let rosso = Colore::Rosso;
let giallo = Colore::Giallo;
let verde = Colore::Verde;
println!("{:?}", rosso);
}
Ricordo che l’istruzione #[derive(Debug)]
è un attributo che si applica a strutture o enumerazioni per auto-generare
automaticamente l’implementazione del trait Debug. Questo trait permette di
stampare le informazioni di una struttura in un formato utile per il
debugging, utilizzando il formattatore {:?}. ma dei formattatori
riparleremo.
Come sempre, è possibile definire la mutabilità degli
elelementi che create partendo da un enumeratore, naturalmente ci
vuole il consueto mut prendendo in
considerazione l'esempio 16.2
potremmo modificare come segue, vi lascio il piacere della prova:let mut rosso = Colore::Rosso;;
rosso = Colore::Giallo
Ora, visto che degli enumeratori trovo difficile parlare senza
esempi, vediamo un super-classico che troverete ovunque che ci mostra la
cooperazione con il pattern matching (e vedremo anche
if-let tra qualche paragrafo) e intanto mettiamoci dentro un ricordo
dei mitici
883. Anche qui ignorate i warning:
Esempio 18.3
enum Direzione {
Nord,
Sud,
Ovest,
Est,
}
fn main() {
let d = Direzione::Nord;
match d {
Direzione::Nord = > println!("Vai a nord"),
Direzione::Sud = > println!("Vai a sud"),
Direzione::Est = > println!("Vai a est"),
Direzione::Ovest = > println!("Vai a ovest"),
}
}
La copertura dei casi deve essere completa diversamente vi prenderete un
errore di
non-exhaustive patterns.
Vediamo adesso un esempio in cui facciamo uso di tipi preimpostati:
Esempio 18.4
enum Messaggio {
Quit,
Move(i32, i32),
Write(String),
}
fn main() {
let m = Messaggio::Move(10, 20);
match m {
Messaggio::Quit = > println!("Stop"),
Messaggio::Move(x, y) = > println!("Muovi a ({x}, {y})"),
Messaggio::Write(s) = > println!("Scrivi: {s}"),
}
let m2 = Messaggio::Write(String::from("Ciao"));
match m2 {
Messaggio::Quit = > println!("Stop"),
Messaggio::Move(x, y) = > println!("Muovi a ({x}, {y})"),
Messaggio::Write(s) = > println!("Scrivi: {s}"),
}
}
Vi lascio il semplice caso in cui viene utilizzato il primo ramo, quello dove trovate Quit.
Come sempre, mi piace affrontare il problema della copia. Le regole sono sempre le stesse. Se tutte le varianti sono Copy lo è anche l'enumeratore e qui non ci sono problemi.
#[derive(Copy, Clone)]
enum Direzione {
Nord,
Sud,
Est,
Ovest,
}
let a = Direzione::Nord;
let b = a; // copia bitwise
qui non ci sono problemi le 4 varianti sono discriminanti che non danno
problemi. La copia avviene senza problemi.
Il caso critico avviene in
una situazione simile:
enum Messaggio {
Testo(String),
Numero(i32),
}
String non è Copy quindi nemmeno l'enumeratore Messaggio può esserlo. Quindi:
let m1 = Messaggio::Testo("ciao".to_string());
let m2 = m1; //
move, non copia
In questi casi la soluzione è clone():
let m2 = m1.clone(); // deep copy del buffer della String
E' possibile, qualora si voglia evitare la copia e ci si debba affidare solo alla lettura usare il borrowing:
let m2 = &m1;
Un altro discorso riguarda la possibilità di implementazione della copia per via interna, ma è un discorso da affrontare dopo aver parlato delle funzioni.
Per ora è tutto ma mancano alcuni pezzi. Ad esempio ritroveremo gli enumeratori al lavoro insieme alle funzioni, l'avrete già capito è argomento costante con i vari costrutti. E poi esplicitata la possibilità, accennata poco fa, di accettare a loro volta delle funzioni all'interno, cosa non comune in altri linguaggi. Option, Result... c'è tanta carne al fuoco per gli enumeratori.