Salta ai contenuti

Panic in Rust

Un panic è il modo in cui Rust ferma subito un programma quando qualcosa è andato storto in modo irrimediabile. Se hai seguito le pagine precedenti, probabilmente ne hai già causato qualcuno senza volerlo: leggere oltre la fine di un array o chiamare .unwrap() su un None provocano entrambi un panic.

fn main() {
panic!("qualcosa è andato storto di brutto");
}
thread 'main' (12345) panicked at src/main.rs:2:5:
qualcosa è andato storto di brutto
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust scrive in inglese che il thread main è andato in panic, dove (file, riga e colonna) e con quale messaggio; l’ultima riga suggerisce di impostare RUST_BACKTRACE=1 per vedere la catena di chiamate. Il numero tra parentesi, qui (12345), è l’ID del thread. A ogni esecuzione vedrai un numero diverso, e le versioni più vecchie di Rust non lo mostrano.

▶ Provalo tu

Quando questo codice gira, il programma stampa quel messaggio e si ferma: niente di quello che c’è dopo panic! viene eseguito.

Cose che vanno in panic senza che tu chiami panic!

Sezione intitolata “Cose che vanno in panic senza che tu chiami panic!”

Alcune operazioni che conosci già vanno in panic da sole se qualcosa non va. Ogni esempio qui sotto riceve il valore rischioso tramite un parametro di funzione invece che come letterale scritto nel codice: con un indice letterale o uno 0 letterale proprio lì, il compilatore spesso capisce già in fase di compilazione che l’operazione fallisce sempre, e si rifiuta di compilare invece di lasciarla girare e andare in panic.

fn get(numbers: &[i32], index: usize) -> i32 {
numbers[index] // panic: indice fuori dai limiti
}
fn main() {
let numbers = [1, 2, 3];
println!("{}", get(&numbers, 10));
}
▶ Provalo tu
fn main() {
let value: Option<i32> = None;
println!("{}", value.unwrap()); // panic: called `Option::unwrap()` on a `None` value
}
▶ Provalo tu
fn divide(a: i32, b: i32) -> i32 {
a / b // panic: attempt to divide by zero
}
fn main() {
let a = 10;
let b = 0;
println!("{}", divide(a, b));
}
▶ Provalo tu

Nei commenti trovi i messaggi originali (in inglese) che Rust stampa: unwrap() chiamato su un None e «tentativo di dividere per zero». In tutti questi casi Rust si rifiuta di far continuare il programma in uno stato che non ha senso, invece di restituire in silenzio valori spazzatura.

Questa è la vera decisione che prenderai spesso: questo fallimento deve far crollare il programma, o chi chiama deve avere la possibilità di gestirlo?

  • panic!: per i bug e le situazioni che non dovrebbero mai accadere se il codice è corretto. Non c’è un modo sensato di riprendersi, quindi fermarsi subito (prima di fare altri danni) è la scelta più sicura.
  • Result (lo trovi nella pagina precedente): per i fallimenti che chi chiama può ragionevolmente aspettarsi e voler gestire, come un input sbagliato dell’utente, un file mancante o una richiesta di rete fallita.
// Input sbagliato da un utente: si può recuperare, usa Result
fn parse_age(input: &str) -> Result<u32, String> {
input.parse().map_err(|_| String::from("non è un'età valida"))
}
// Un'invariante che dovrebbe essere impossibile se il codice è corretto: panic
fn get_first(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
panic!("get_first chiamata con una slice vuota");
}
numbers[0]
}

unwrap() ed expect() sono scorciatoie per il panic

Sezione intitolata “unwrap() ed expect() sono scorciatoie per il panic”

Hai già visto .unwrap() sia su Option sia su Result: in pratica è solo una scorciatoia per «dammi il valore, oppure vai in panic se non c’è». .expect("messaggio") fa la stessa cosa ma ti permette di aggiungere un messaggio più utile:

fn main() {
let input = "42";
let number: i32 = input.parse().expect("l'input dovrebbe essere un numero valido");
println!("{number}");
}
▶ Provalo tu

Se la conversione fallisce, il messaggio del panic contiene il tuo testo, e questo rende il debug molto più facile rispetto al messaggio generico di .unwrap().

Verificare le tue ipotesi: assert!, assert_eq! e unreachable!

Sezione intitolata “Verificare le tue ipotesi: assert!, assert_eq! e unreachable!”

A volte sai che una cosa deve essere vera in un certo punto del codice. Rust ha alcune piccole macro che trasformano questa convinzione in un controllo. Se il controllo fallisce, vanno in panic con un messaggio chiaro, così il bug salta fuori proprio dove nasce invece di causare risultati strani più avanti.

  • assert!(condizione, "messaggio") va in panic se la condizione è false.
  • assert_eq!(left, right) va in panic se i due valori non sono uguali, e li stampa entrambi.
  • unreachable!("messaggio") segna codice che non dovrebbe mai essere eseguito, come un ramo di match per un valore impossibile.
fn average(numbers: &[i32]) -> i32 {
assert!(!numbers.is_empty(), "average ha bisogno di almeno un numero");
let mut total = 0;
for n in numbers {
total += n;
}
total / numbers.len() as i32
}
fn day_name(day: u8) -> &'static str {
match day {
1 => "lunedì",
2 => "martedì",
3 => "mercoledì",
4 => "giovedì",
5 => "venerdì",
6 => "sabato",
7 => "domenica",
_ => unreachable!("il giorno deve andare da 1 a 7, invece è {day}"),
}
}
fn main() {
let avg = average(&[70, 80, 90]);
assert_eq!(avg, 80);
println!("media: {avg}");
println!("il giorno 3 è {}", day_name(3));
}

Output:

media: 80
il giorno 3 è mercoledì
▶ Provalo tu

Qui tutti i controlli passano, quindi il programma gira normalmente. Ora nel Playground cambia la lista in [70, 80, 95]. La media diventa 81 e assert_eq! ferma il programma mostrando i due valori uno accanto all’altro:

thread 'main' (12345) panicked at src/main.rs:26:5:
assertion `left == right` failed
left: 81
right: 80

Il messaggio (in inglese) dice che l’asserzione left == right è fallita: a sinistra c’è 81, a destra 80. Prova anche average(&[]) o day_name(9), e vedrai i tuoi messaggi nel panic.

Questi controlli girano sempre, anche in una build di release, quindi tienili leggeri. Ritroverai assert_eq! nella pagina sui test, dove è il modo principale con cui un test dice «questo deve essere uguale a quello». C’è anche todo!(), che va in panic con «not yet implemented» (non ancora implementato). È comodo come corpo provvisorio mentre abbozzi una funzione che finirai più tardi.

Hai un valore di configurazione let port_text = "8080";. Trasformalo in un u16 con .parse() ed .expect(...), usando un messaggio che spieghi cosa è andato storto. Poi stampa In ascolto sulla porta 8080. Suggerimento: la variabile ha bisogno di un tipo, let port: u16 = ..., così parse sa cosa produrre. Dopo, cambia il testo in "80a0" per vedere il tuo messaggio nel panic.

Mostra la soluzione
fn main() {
let port_text = "8080";
let port: u16 = port_text
.parse()
.expect("la porta nella configurazione dovrebbe essere un numero da 0 a 65535");
println!("In ascolto sulla porta {port}");
}

Output:

In ascolto sulla porta 8080
▶ Provalo tu

Scrivi fn split_bill(total: u32, people: u32) -> u32, che restituisce la quota di ogni persona. Dividere tra zero persone è un bug nel codice che chiama, quindi usa assert! con un messaggio chiaro per scoprirlo. Stampa la quota per un totale di 120 e 4 persone. Suggerimento: metti l’assert! sulla prima riga della funzione, prima della divisione.

Mostra la soluzione
fn split_bill(total: u32, people: u32) -> u32 {
assert!(people > 0, "split_bill ha bisogno di almeno una persona");
total / people
}
fn main() {
let share = split_bill(120, 4);
println!("Ognuno paga {share}");
}

Output:

Ognuno paga 30
▶ Provalo tu
  • panic! ferma subito il programma con un messaggio di errore.
  • Alcune operazioni standard (indice di array non valido, .unwrap() su None, divisione per zero) vanno in panic automaticamente.
  • Usa panic! per bug e invarianti che dovrebbero essere impossibili; usa Result per i fallimenti che chi chiama può ragionevolmente gestire.
  • .expect("messaggio") è come .unwrap(), ma con un messaggio che rende il panic più facile da indagare.
  • assert!, assert_eq! e unreachable! vanno in panic apposta quando una cosa che credevi vera si rivela falsa.

Verifica veloce

1. Cosa fa una chiamata a panic!?

2. Secondo la regola pratica di questa pagina, quando conviene usare panic! invece di Result?

3. Qual è la differenza tra .unwrap() ed .expect('messaggio')?

Punteggio: 0 / 3