Result in Rust
Il tipo Result<T, E> e lo strumento principale di Rust per la gestione degli errori recuperabili. Mentre Option rappresenta la presenza o assenza di un valore, Result rappresenta il successo o il fallimento di un’operazione, portando con se informazioni sull’errore.
Ok(T) e Err(E)
Result<T, E> e un enum con due varianti:
// Definizione nella libreria standard
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
fn main() {
let successo: Result<i32, String> = Ok(42);
let fallimento: Result<i32, String> = Err(String::from("qualcosa e andato storto"));
println!("Successo: {:?}", successo); // Ok(42)
println!("Fallimento: {:?}", fallimento); // Err("qualcosa e andato storto")
}
Il Pattern di Gestione degli Errori
Il pattern piu comune e restituire Result da funzioni che possono fallire:
use std::num::ParseIntError;
fn analizza_numero(testo: &str) -> Result<i32, ParseIntError> {
testo.trim().parse::<i32>()
}
fn main() {
match analizza_numero("42") {
Ok(n) => println!("Numero: {}", n),
Err(e) => println!("Errore di parsing: {}", e),
}
match analizza_numero("abc") {
Ok(n) => println!("Numero: {}", n),
Err(e) => println!("Errore di parsing: {}", e),
}
}
unwrap e expect
Come per Option, unwrap e expect estraggono il valore da Ok o causano un panic su Err:
fn main() {
let ok: Result<i32, &str> = Ok(10);
let err: Result<i32, &str> = Err("errore");
// unwrap: estrae il valore o va in panic
println!("Valore: {}", ok.unwrap()); // 10
// expect: panic con messaggio personalizzato
// err.expect("Operazione fallita!"); // PANIC con messaggio
// Controllo sicuro
println!("E Ok: {}", ok.is_ok()); // true
println!("E Err: {}", err.is_err()); // true
}
Pattern Matching con Result
Il match e il modo piu esplicito per gestire entrambi i casi:
use std::fs;
fn leggi_configurazione(percorso: &str) -> Result<String, String> {
fs::read_to_string(percorso)
.map_err(|e| format!("Impossibile leggere {}: {}", percorso, e))
}
fn main() {
match leggi_configurazione("config.toml") {
Ok(contenuto) => {
println!("Configurazione caricata:");
println!("{}", contenuto);
}
Err(errore) => {
eprintln!("Errore: {}", errore);
}
}
}
Metodi Combinatori: map, map_err, and_then
I metodi combinatori permettono di trasformare Result in modo funzionale:
fn main() {
let numero: Result<i32, String> = Ok(5);
let errore: Result<i32, String> = Err(String::from("errore"));
// map: trasforma il valore Ok
let doppio = numero.map(|n| n * 2);
println!("Doppio: {:?}", doppio); // Ok(10)
// map_err: trasforma il valore Err
let err_formattato = errore.map_err(|e| format!("ERRORE: {}", e));
println!("{:?}", err_formattato); // Err("ERRORE: errore")
// and_then (flatmap): concatena operazioni che possono fallire
let risultato = Ok("42")
.and_then(|s: &str| {
s.parse::<i32>().map_err(|e| e.to_string())
})
.and_then(|n| {
if n > 0 {
Ok(n * 2)
} else {
Err(String::from("Il numero deve essere positivo"))
}
});
println!("Risultato: {:?}", risultato); // Ok(84)
// unwrap_or e unwrap_or_else
let val = errore.unwrap_or(0);
println!("Con default: {}", val); // 0
let val = errore.unwrap_or_else(|e| {
eprintln!("Errore ignorato: {}", e);
-1
});
println!("Con default calcolato: {}", val); // -1
}
L’Operatore ?
L’operatore ? e il modo piu idiomatico per propagare gli errori. Posto dopo un’espressione Result, estrae il valore Ok oppure restituisce immediatamente l’errore Err dalla funzione corrente.
use std::fs;
use std::io;
fn leggi_nome_utente(percorso: &str) -> Result<String, io::Error> {
let contenuto = fs::read_to_string(percorso)?;
Ok(contenuto.trim().to_string())
}
// Concatenare piu operazioni fallibili
fn prima_riga(percorso: &str) -> Result<String, io::Error> {
let contenuto = fs::read_to_string(percorso)?;
let riga = contenuto
.lines()
.next()
.ok_or(io::Error::new(io::ErrorKind::InvalidData, "File vuoto"))?;
Ok(riga.to_string())
}
fn main() {
match leggi_nome_utente("utente.txt") {
Ok(nome) => println!("Nome utente: {}", nome),
Err(e) => eprintln!("Errore lettura: {}", e),
}
}
L’operatore ? richiede che il tipo di errore sia compatibile. Se i tipi sono diversi, Rust usa il trait From per la conversione automatica.
Conversione tra Option e Result
Rust fornisce metodi per convertire tra Option e Result:
fn main() {
// Option -> Result
let opzione: Option<i32> = Some(42);
let risultato: Result<i32, &str> = opzione.ok_or("valore assente");
println!("{:?}", risultato); // Ok(42)
let niente: Option<i32> = None;
let risultato: Result<i32, &str> = niente.ok_or("valore assente");
println!("{:?}", risultato); // Err("valore assente")
// ok_or_else con closure (lazy evaluation)
let risultato = niente.ok_or_else(|| {
format!("Errore generato alle {:?}", std::time::SystemTime::now())
});
println!("{:?}", risultato);
// Result -> Option
let ok: Result<i32, &str> = Ok(10);
let opzione = ok.ok(); // Some(10)
println!("{:?}", opzione);
let err: Result<i32, &str> = Err("errore");
let opzione = err.ok(); // None
println!("{:?}", opzione);
// Anche per il lato errore
let errore_opzione = err.err(); // Some("errore")
println!("{:?}", errore_opzione);
}
Esempio Pratico: Parsing di Configurazione
use std::collections::HashMap;
#[derive(Debug)]
struct Config {
host: String,
porta: u16,
max_connessioni: u32,
}
fn analizza_config(input: &str) -> Result<Config, String> {
let mut mappa: HashMap<&str, &str> = HashMap::new();
for riga in input.lines() {
let parti: Vec<&str> = riga.splitn(2, '=').collect();
if parti.len() == 2 {
mappa.insert(parti[0].trim(), parti[1].trim());
}
}
let host = mappa.get("host")
.ok_or("Campo 'host' mancante")?
.to_string();
let porta = mappa.get("porta")
.ok_or("Campo 'porta' mancante")?
.parse::<u16>()
.map_err(|e| format!("Porta non valida: {}", e))?;
let max_connessioni = mappa.get("max_connessioni")
.ok_or("Campo 'max_connessioni' mancante")?
.parse::<u32>()
.map_err(|e| format!("max_connessioni non valido: {}", e))?;
Ok(Config { host, porta, max_connessioni })
}
fn main() {
let input = "host = localhost\nporta = 8080\nmax_connessioni = 100";
match analizza_config(input) {
Ok(config) => println!("Config: {:?}", config),
Err(e) => eprintln!("Errore: {}", e),
}
}
Conclusione
Result<T, E> e il fondamento della gestione degli errori in Rust. A differenza delle eccezioni di altri linguaggi, gli errori in Rust sono valori espliciti che devono essere gestiti. L’operatore ? rende la propagazione degli errori concisa e leggibile, i metodi combinatori come map, map_err e and_then permettono trasformazioni eleganti, e la conversione tra Option e Result offre grande flessibilita. La regola fondamentale: usa Result per errori recuperabili, panic! solo per situazioni irrecuperabili.