Salta ai contenuti

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».

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 nord
▶ Provalo tu

Nota 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.

È 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.27
20.00
▶ Provalo tu

Shape::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».

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 secondi
Green per 25 secondi
Yellow per 3 secondi
Red per 30 secondi
▶ Provalo tu

next 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.

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.

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 centesimi
▶ Provalo tu

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.28
18.00
12.00
▶ Provalo tu

Prova 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.

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 °C
100.0 °C
10.0 °C
▶ Provalo tu

Visto 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».

  • 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.
  • match destruttura 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 (e Result, che vedremo più avanti) sono semplici enum della libreria standard: non hanno niente di magico.

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