Option in Rust
Il tipo Option<T> e il modo in cui Rust gestisce i valori che possono essere presenti o assenti. A differenza di molti linguaggi che usano null o nil, Rust rende esplicita l’assenza di un valore attraverso il sistema dei tipi, eliminando un’intera classe di bug a tempo di compilazione.
Some(T) e None
Option<T> e un enum definito nella libreria standard con due varianti:
// Definizione nella libreria standard (non serve riscriverla)
// enum Option<T> {
// Some(T),
// None,
// }
fn main() {
let numero: Option<i32> = Some(42);
let niente: Option<i32> = None;
println!("Numero: {:?}", numero); // Some(42)
println!("Niente: {:?}", niente); // None
// Option e cosi comune che Some e None sono nel prelude
// Non serve scrivere Option::Some o Option::None
let nome = Some("Rust");
let vuoto: Option<&str> = None;
}
Perche Rust Non Ha null
In molti linguaggi, null puo apparire ovunque, causando errori a runtime (NullPointerException). In Rust, un valore di tipo T e sempre valido. Se un valore puo essere assente, bisogna usare esplicitamente Option<T>, costringendo il programmatore a gestire il caso None.
fn trova_utente(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Marco"))
} else {
None
}
}
fn main() {
let utente = trova_utente(1);
// Non puoi usare utente direttamente come String
// Devi prima gestire il caso None
// let nome: String = utente; // ERRORE di compilazione!
// Modo corretto:
match utente {
Some(nome) => println!("Trovato: {}", nome),
None => println!("Utente non trovato"),
}
}
unwrap e expect
I metodi unwrap e expect estraggono il valore da un Some, ma causano un panic se il valore e None. Usali solo quando sei certo che il valore sia presente.
fn main() {
let x: Option<i32> = Some(10);
let y: Option<i32> = None;
// unwrap: estrae il valore o va in panic
println!("x = {}", x.unwrap()); // 10
// expect: come unwrap ma con messaggio personalizzato
// println!("y = {}", y.expect("Il valore doveva essere presente!"));
// PANIC: Il valore doveva essere presente!
// is_some e is_none per controlli sicuri
println!("x ha un valore: {}", x.is_some()); // true
println!("y e None: {}", y.is_none()); // true
}
Pattern Matching con Option
Il pattern matching e il modo piu esplicito e sicuro per gestire Option:
fn dividi(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
let risultato = dividi(10.0, 3.0);
match risultato {
Some(valore) => println!("Risultato: {:.2}", valore),
None => println!("Impossibile dividere per zero"),
}
// Con guard condition
match dividi(100.0, 7.0) {
Some(v) if v > 10.0 => println!("Risultato grande: {:.2}", v),
Some(v) => println!("Risultato: {:.2}", v),
None => println!("Errore"),
}
}
Metodi Combinatori: map, and_then, unwrap_or
Rust offre metodi funzionali per trasformare e combinare Option senza match espliciti:
fn main() {
let numero: Option<i32> = Some(5);
let niente: Option<i32> = None;
// map: trasforma il valore interno
let doppio = numero.map(|n| n * 2);
println!("Doppio: {:?}", doppio); // Some(10)
println!("Doppio di niente: {:?}", niente.map(|n| n * 2)); // None
// unwrap_or: fornisce un valore di default
println!("Valore: {}", numero.unwrap_or(0)); // 5
println!("Default: {}", niente.unwrap_or(0)); // 0
// unwrap_or_else: default calcolato con closure (lazy)
let val = niente.unwrap_or_else(|| {
println!("Calcolo il default...");
42
});
println!("Valore calcolato: {}", val);
// and_then (flatmap): per operazioni che restituiscono Option
let risultato = Some("42")
.and_then(|s| s.parse::<i32>().ok())
.map(|n| n * 2);
println!("Risultato: {:?}", risultato); // Some(84)
// filter: mantiene il valore solo se la condizione e vera
let pari = Some(4).filter(|n| n % 2 == 0);
let dispari = Some(3).filter(|n| n % 2 == 0);
println!("Pari: {:?}, Dispari: {:?}", pari, dispari); // Some(4), None
}
L’Operatore ? con Option
L’operatore ? puo essere usato con Option in funzioni che restituiscono Option. Se il valore e None, la funzione restituisce None immediatamente.
#[derive(Debug)]
struct Indirizzo {
citta: String,
}
#[derive(Debug)]
struct Utente {
nome: String,
indirizzo: Option<Indirizzo>,
}
fn citta_utente(utente: &Option<Utente>) -> Option<&str> {
let utente = utente.as_ref()?;
let indirizzo = utente.indirizzo.as_ref()?;
Some(&indirizzo.citta)
}
fn main() {
let utente = Some(Utente {
nome: String::from("Anna"),
indirizzo: Some(Indirizzo {
citta: String::from("Milano"),
}),
});
let senza_indirizzo = Some(Utente {
nome: String::from("Luca"),
indirizzo: None,
});
println!("Citta: {:?}", citta_utente(&utente)); // Some("Milano")
println!("Citta: {:?}", citta_utente(&senza_indirizzo)); // None
println!("Citta: {:?}", citta_utente(&None)); // None
}
if let e while let con Option
Per gestire solo il caso Some senza un match completo, si usano if let e while let:
fn main() {
let configurazione: Option<&str> = Some("produzione");
// if let: gestisce solo Some
if let Some(env) = configurazione {
println!("Ambiente: {}", env);
} else {
println!("Nessuna configurazione impostata");
}
// while let: itera finche il valore e Some
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(valore) = stack.pop() {
println!("Estratto: {}", valore);
}
println!("Stack vuoto!");
}
Concatenare Operazioni su Option
Un esempio pratico di concatenazione di operazioni:
fn primo_numero_pari(testo: &str) -> Option<i32> {
testo
.split_whitespace()
.filter_map(|s| s.parse::<i32>().ok())
.find(|n| n % 2 == 0)
}
fn main() {
let testo = "ho 3 gatti e 4 cani e 7 pesci";
match primo_numero_pari(testo) {
Some(n) => println!("Primo numero pari trovato: {}", n),
None => println!("Nessun numero pari trovato"),
}
}
Conclusione
Option<T> e il pilastro della sicurezza contro i valori nulli in Rust. Usandolo correttamente, si eliminano i null pointer error a tempo di compilazione. I metodi combinatori come map, and_then, unwrap_or e l’operatore ? permettono di scrivere codice conciso ed espressivo senza sacrificare la sicurezza. La regola d’oro: evita unwrap() nel codice di produzione e preferisci sempre la gestione esplicita dei casi None.