Salta ai contenuti

Result in Rust

Se Option rappresenta un valore che potrebbe mancare, Result rappresenta un’operazione che potrebbe fallire. E a differenza di un valore mancante, un fallimento di solito porta con sé un motivo.

Result è un’altra enum della libreria standard, questa volta con due varianti che contengono entrambe dei dati:

enum Result<T, E> {
Ok(T), // successo, con un valore
Err(E), // fallimento, con un errore
}

.parse(), che hai incontrato nella pagina sul cast dei tipi, è un buon esempio reale: trasformare un testo in un numero può fallire se il testo non è davvero un numero:

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

Output:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Provalo tu

InvalidDigit è il modo in cui Rust ti dice (in inglese) che nel testo c’è una cifra non valida.

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("Convertito: {number}"),
Err(e) => println!("Conversione fallita: {e}"),
}
}
▶ Provalo tu

È la stessa forma che usi per gestire un Option con match, ma il ramo Err ti dà un vero valore di errore da esaminare o segnalare, non solo «non c’era niente».

fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("impossibile dividere per zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Risultato: {result}"),
Err(e) => println!("Errore: {e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("Risultato: {result}"),
Err(e) => println!("Errore: {e}"),
}
}

Output:

Risultato: 5
Errore: impossibile dividere per zero
▶ Provalo tu

Restituire Result<f64, String> obbliga chiunque chiami la funzione a fare i conti con un possibile fallimento: non c’è modo di trattare per sbaglio una divisione per zero come un numero normale.

Concatenare diverse operazioni che possono fallire porta a blocchi match annidati su più livelli. L’operatore ? li appiattisce: vuol dire «se questo è Err, restituisci subito quell’errore dalla funzione corrente; altrimenti dammi il valore dentro Ok».

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // esce subito se la conversione fallisce
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("ops"));
}

Output:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Provalo tu

Senza ?, quella riga avrebbe bisogno del suo match solo per estrarre number o uscire in anticipo con l’errore: ? fa esattamente questo in un solo carattere. Funziona solo dentro una funzione che a sua volta restituisce un Result (o un Option), perché è lì che può restituire l’errore.

Rust ha anche panic!, che ferma subito il programma: lo vedi più in dettaglio nella prossima pagina. Come regola pratica: usa Result per i fallimenti che chi chiama potrebbe ragionevolmente voler gestire (input sbagliato, un file mancante); tieni panic! per le situazioni che indicano un vero bug, in cui continuare sarebbe peggio che fermarsi.

Quando le tue funzioni possono fallire in tanti modi diversi, una semplice String come errore non basta più. La pagina sugli errori personalizzati mostra come definire un tuo tipo di errore, e quella su anyhow e thiserror mostra i crate che quasi tutti i progetti Rust usano per scrivere meno codice ripetitivo.

Scrivi fn parse_age(input: &str) -> Result<u32, String>. Deve restituire Err con un messaggio utile se il testo non è un numero, o se il numero supera 150. Provala con "34", "abc" e "200". Suggerimento: fai match su input.parse::<u32>() e, dentro il ramo Ok, usa un if per controllare il valore.

Mostra la soluzione
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age} anni sono troppi per essere veri"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("'{input}' non è un numero")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("Età: {age}"),
Err(e) => println!("Errore: {e}"),
}
}
}

Output:

Età: 34
Errore: 'abc' non è un numero
Errore: 200 anni sono troppi per essere veri
▶ Provalo tu

Scrivi fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>, che converte entrambe le stringhe e restituisce la loro somma. Usa ? invece di match. Stampa il risultato per ("10", "20") e per ("10", "venti"). Suggerimento: ti serve un ? per ogni parse, e l’ultima riga avvolge la somma in Ok(...).

Mostra la soluzione
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "venti"));
}

Output:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ Provalo tu
  • Result<T, E> rappresenta un’operazione che può riuscire (Ok) o fallire (Err) con un valore di errore.
  • match gestisce entrambi i casi in modo esplicito, con lo stesso schema di Option.
  • L’operatore ? restituisce subito un Err in automatico, così non ti serve un match dopo ogni chiamata che può fallire.
  • Preferisci Result a panic! per gli errori da cui chi chiama potrebbe ragionevolmente riprendersi.

Verifica veloce

1. Quali sono le due varianti di Result<T, E>?

2. Cosa fa l'operatore ? dentro una funzione che restituisce un Result?

3. Secondo questa pagina, quando conviene usare Result invece di panic!?

4. Nell'esempio divide di questa pagina, cosa restituisce divide(10.0, 0.0)?

5. Perché l'operatore ? funziona solo dentro una funzione che a sua volta restituisce un Result (o un Option)?

Punteggio: 0 / 5