Enum in Rust
Un enum (abbreviazione di enumerazione) definisce un tipo che può essere una sola tra un insieme fisso di possibilità. Se una struct serve per dire «questa cosa ha tutti questi campi», un enum serve per dire «questa cosa è esattamente una di queste opzioni».
Definire e usare un enum
Sezione intitolata “Definire e usare un enum”enum Direction { North, South, East, West,}
fn main() { let heading = Direction::North;
match heading { Direction::North => println!("Si va a nord"), Direction::South => println!("Si va a sud"), Direction::East => println!("Si va a est"), Direction::West => println!("Si va a ovest"), }}Output:
Si va a nordNota che qui match non ha bisogno di un ramo _ che prende tutto il resto: visto che ogni possibile Direction è elencata, Rust capisce già che il match è completo.
Gli enum possono contenere dati
Sezione intitolata “Gli enum possono contenere dati”È qui che gli enum di Rust vanno oltre i classici enum di altri linguaggi: ogni variante può portarsi dietro i suoi dati:
enum Shape { Circle(f64), // raggio Rectangle(f64, f64), // larghezza, altezza}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => 3.14159 * radius * radius, Shape::Rectangle(width, height) => width * height, }}
fn main() { let circle = Shape::Circle(3.0); let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle)); println!("{:.2}", area(&rectangle));}Output:
28.2720.00Shape::Circle porta con sé un f64 (il raggio); Shape::Rectangle ne porta due. match tira fuori quei dati per te direttamente in ogni ramo: non serve un passaggio separato per «scartarli».
Anche gli enum possono avere metodi
Sezione intitolata “Anche gli enum possono avere metodi”Proprio come una struct, un enum può avere un blocco impl con dei metodi. Dentro un metodo di solito fai match self per decidere cosa fare per ogni variante:
#[derive(Debug)]enum Light { Red, Yellow, Green,}
impl Light { fn seconds(&self) -> u32 { match self { Light::Red => 30, Light::Yellow => 3, Light::Green => 25, } }
fn next(&self) -> Light { match self { Light::Red => Light::Green, Light::Green => Light::Yellow, Light::Yellow => Light::Red, } }}
fn main() { let mut light = Light::Red;
for _ in 0..4 { println!("{:?} per {} secondi", light, light.seconds()); light = light.next(); }}Output:
Red per 30 secondiGreen per 25 secondiYellow per 3 secondiRed per 30 secondinext restituisce un Light nuovo di zecca, e light = light.next(); sostituisce quello vecchio. Per questo light è dichiarata mut. #[derive(Debug)] funziona sugli enum come sulle struct, quindi {:?} stampa il nome della variante (qui Red, Green e Yellow, cioè rosso, verde e giallo).
Mettere la logica nei metodi la tiene vicina al tipo. Chiunque usi un Light può chiamare light.seconds() senza sapere come vengono decisi i tempi. E se più avanti aggiungi una nuova variante, come FlashingYellow, il compilatore ti indica ogni match da aggiornare.
Option: un enum che stai già usando
Sezione intitolata “Option: un enum che stai già usando”Il tipo Option, che vedrai per bene nella sua pagina dedicata, è semplicemente un enum definito nella libreria standard:
enum Option<T> { Some(T), None,}Una volta capiti gli enum, Option smette di sembrare una sintassi speciale: è un enum del tutto normale con due varianti, una delle quali porta con sé un valore.
Perché non usare una struct con un campo «tipo»?
Sezione intitolata “Perché non usare una struct con un campo «tipo»?”Potresti pensare di simulare tutto questo con una struct e un campo stringa come "cerchio" o "rettangolo", ma allora niente ti impedirebbe di scrivere per sbaglio "cercho", e dovresti controllare a mano quali campi hanno davvero senso per ogni tipo. Un enum rende impossibile già in partenza scrivere combinazioni non valide, e il compilatore ti obbliga a gestire ogni variante.
Pratica
Sezione intitolata “Pratica”1. Conta le monete
Sezione intitolata “1. Conta le monete”Crea un enum Coin con le varianti Penny, Nickel, Dime e Quarter (le monete americane da 1, 5, 10 e 25 centesimi). Scrivi fn value_in_cents(coin: &Coin) -> u32, poi somma il contenuto di un borsellino: un quarter, un dime, un nickel, un penny e un altro quarter. Suggerimento: salva le monete in un array e scorri &purse.
Mostra la soluzione
enum Coin { Penny, Nickel, Dime, Quarter,}
fn value_in_cents(coin: &Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }}
fn main() { let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0; for coin in &purse { total += value_in_cents(coin); }
println!("Totale: {total} centesimi");}Output:
Totale: 66 centesimi2. Aggiungi un triangolo
Sezione intitolata “2. Aggiungi un triangolo”Prendi l’enum Shape di questa pagina e aggiungi una variante Triangle che contiene le lunghezze dei suoi tre lati. Poi scrivi una funzione perimeter (la lunghezza di tutto il contorno) che funziona per ogni forma. Suggerimento: il perimetro di un cerchio è 2.0 * 3.14159 * radius. Quello di un triangolo è la somma dei suoi tre lati.
Mostra la soluzione
enum Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64, f64),}
fn perimeter(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => 2.0 * 3.14159 * radius, Shape::Rectangle(width, height) => 2.0 * (width + height), Shape::Triangle(a, b, c) => a + b + c, }}
fn main() { let shapes = [ Shape::Circle(1.0), Shape::Rectangle(4.0, 5.0), Shape::Triangle(3.0, 4.0, 5.0), ];
for shape in &shapes { println!("{:.2}", perimeter(shape)); }}Output:
6.2818.0012.00Prova a cancellare il ramo Shape::Triangle dal match. Rust si rifiuta di compilare, perché il match non copre più tutte le varianti. È il compilatore che si assicura che tu non dimentichi mai un caso.
3. Temperature
Sezione intitolata “3. Temperature”Crea un enum Temperature con due varianti che contengono ciascuna un f64: Celsius e Fahrenheit. Aggiungi un metodo to_celsius(&self) -> f64 e stampa Celsius(25.0), Fahrenheit(212.0) e Fahrenheit(50.0) in °C con una cifra decimale. Suggerimento: per convertire usa (f - 32.0) * 5.0 / 9.0.
Mostra la soluzione
enum Temperature { Celsius(f64), Fahrenheit(f64),}
impl Temperature { fn to_celsius(&self) -> f64 { match self { Temperature::Celsius(c) => *c, Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0, } }}
fn main() { let readings = [ Temperature::Celsius(25.0), Temperature::Fahrenheit(212.0), Temperature::Fahrenheit(50.0), ];
for t in &readings { println!("{:.1} °C", t.to_celsius()); }}Output:
25.0 °C100.0 °C10.0 °CVisto che self è preso in prestito, c e f sono riferimenti (&f64). I calcoli funzionano direttamente sui riferimenti, ma per restituire il semplice valore in Celsius scrivi *c, che significa «il numero a cui punta c».
Riepilogo
Sezione intitolata “Riepilogo”- Un enum definisce un tipo come una tra diverse varianti con un nome:
enum Direction { North, South, ... }. - Le varianti possono portare con sé dei dati, e varianti diverse possono portare dati diversi.
matchdestruttura in modo naturale i dati di un enum e obbliga a gestire ogni variante.- Gli enum possono avere metodi in un blocco
impl, proprio come le struct. Option(eResult, che vedremo più avanti) sono semplici enum della libreria standard: non hanno niente di magico.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Che cosa può fare ogni variante di un enum in Rust, a differenza dei classici enum di altri linguaggi?
2. Perché il match dell'esempio Direction non ha bisogno di un ramo _ che prende tutto il resto?
3. Come è implementato in realtà Option<T> nella libreria standard?
Punteggio: 0 / 3
Per approfondire
Sezione intitolata “Per approfondire”- Defining an Enum — The Rust Book (in inglese): varianti con dati, metodi sugli enum e perché Rust ha
Optioninvece di null. - The
matchControl Flow Construct — The Rust Book (in inglese): il match sugli enum in dettaglio, con un esempio sulle monete e i pattern che prendono tutto il resto. - Enums — Rust by Example (in inglese): esempi eseguibili, comprese varianti con campi con un nome come
Click { x: i64, y: i64 }. - Enumerations — The Rust Reference (in inglese): le regole complete, come assegnare alle varianti valori numerici espliciti.
Option— documentazione della libreria standard (in inglese): la vera definizione dell’enum che userai di più, con tutti i suoi metodi di supporto.