Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Rust e gestione degli errori

Rust: cosa sono gli errori, quando si verificano, in cosa si differenziano tra loro e come gestirli nelle nostre applicazioni
Rust: cosa sono gli errori, quando si verificano, in cosa si differenziano tra loro e come gestirli nelle nostre applicazioni
Link copiato negli appunti

Anche in Rust gli errori in fase di esecuzione sono normali aspetti del funzionamento di un programma. Non dobbiamo pensarli necessariamente come qualcosa "di sbagliato" ma piuttosto come una situazione anomala causata da dati pervenuti dall'esterno o condizioni indotte. Se il nostro programma è in esecuzione significa che sintatticamente è corretto pertanto è stato compilato con successo e lanciato dal runtime.

Eppure, qualcosa di imprevisto può capitare: un software che scarica dati dalla rete può improvvisamente perdere la connessione, lo spazio disponibile del disco su cui stiamo scrivendo file potrebbe esaurirsi e così via.

In questa lezione, scopriamo come il linguaggio Rust permetta di affrontare tali situazioni anomale per poterle gestire ma soprattutto, cosa più importante, evitare che il programma si interrompa in modo incontrollato!

Tipologie di errori I Rust

Scopriamo subito che in Rust si parla di due tipologie di errori: errori unrecoverable e recoverable.

I primi sono quelli che devono decretare la fine del programma mentre i secondi possono essere trattati in maniera alternativa. Nei prossimi due paragrafi, li tratteremo entrambi e soprattutto con le lezioni successive scopriremo molte funzionalità del linguaggio che avranno questa logica di gestione degli errori incorporata. Saranno tutte occasioni propizie per un approfondimento sul tema.

Panic: errori non recuperabili

Nonostante lo scopo sia quello di evitare interruzioni del programma, può capitare che a volte vogliamo noi interrompere volontariamente l'esecuzione in date condizioni. Per questo esiste appositamente la macro panic! che si occupa di arrestare l'esecuzione, ricevendo come argomento anche un messaggio che sarà proposto tra i log del programma:

fn main() {
    println!("Fino a qui tutto bene...");
    panic!("...problema...il programma viene interrotto!");
}

Il programma stamperà il messaggio "Fino a qui tutto bene..." e poi si chiuderà per effetto del panic lasciandone però traccia nel messaggio di uscita:

thread 'main' panicked at src/main.rs:4:5:
...problema...il programma viene interrotto!

Errori recoverable

Nel paragrafo precedente abbiamo scoperto come causare noi stessi l'interruzione del programma, in questo invece vediamo come fare in modo che le operazioni proseguano solo se ci sono le condizioni per farlo. Tipicamente, gli errori sono causati da situazioni in cui una qualche componente con cui collaboriamo restituisce errore o un valore che ci è stato fornito non idoneo allo svolgimento di operazioni. Per fare in modo che il programma prosegua solo in condizioni di sicurezza possiamo basarci su due enum:

  • Option che restituisce Some se c'è un valore disponibile o None in caso contrario. In base a questo potremo sapere se abbiamo elementi sufficienti per andare avanti e se affrontare la situazione;
  • Result che verifica se una determinata operazione ha fornito un risultato o ha dato errore.

Trattandosi di enum, entrambi i casi possono essere gestiti con match ma Rust mette anche altre due funzioni che possono fare al caso nostro: unwrap e expect.

Usando match possiamo ricorrere ad un "grande classico" in cui testiamo la divisione per zero:

fn divisione(dividendo: i32, divisore: i32) -> Result<i32, String> {
    if divisore != 0 {
        Ok(dividendo / divisore)
    } else {
         Err(String::from("Divisione per zero!"))
    }
}
fn main() {
    let dividendo=18;
    let divisore=3;
    let esito = divisione(dividendo, divisore);
    match esito {
        Ok(risultato) => println!("Operazione {}/{}: {}", dividendo,divisore, risultato),
        Err(messaggio) => println!("Errore: {}", messaggio),
    }
}

Notiamo che il risultato della funzione è di tipo Result ma non è affatto scontato. Infatti può trattarsi di un Result in caso di risultato valido o Err in caso di divisione per zero. Sarà proprio il match a giudicare di cosa si tratta in base alla tipologia di riferimento.

Ci potremmo chiedere a questo punto: ma ogni volta che usiamo il Result (per Option varrà lo stesso discorso) siamo costretti ad usare il match? In realtà, no!

Esistono le funzioni unwrap e expect che permettono di estrapolare un risultato da una di queste due enum. In pratica, la funzione dell'esempio precedente potrebbe essere invocata anche così:

let esito = divisione(dividendo, divisore);
println!("Operazione {}/{}: {}", dividendo,divisore, esito.unwrap())

o così:

let esito = divisione(dividendo, divisore);
println!("Operazione {}/{}: {}", dividendo,divisore, esito.expect("Errore!"))

Con la differenza che la seconda accetta una stringa da proporre nel caso in cui l'esito non sia favorevole.

Ti consigliamo anche