English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Gestione degli errori Rust

Rust ha un meccanismo unico per gestire situazioni di eccezione, che non è così semplice come il meccanismo try degli altri linguaggi.

Prima di tutto, ci sono due tipi di errori che si verificano nel programma: errori ripristinabili e errori irreversibili.

Un esempio tipico di errore ripristinabile è l'errore di accesso ai file, se l'accesso a un file fallisce, potrebbe essere perché è in uso, è normale, possiamo risolverlo aspettando.

Ma ci sono anche errori causati da errori logici non risolvibili nel codice di programmazione, ad esempio, accedere a posizioni oltre la fine dell'array.

La maggior parte dei linguaggi di programmazione non distingue tra questi due tipi di errori e li rappresenta con la classe Exception (eccezione). In Rust non esiste Exception.

Per gli errori ripristinabili utilizzare la classe Result<T, E>, per gli errori irreversibili utilizzare il macro panic!.

Errore irreversibile

Fino ad ora, non è stata introdotta la sintassi dei macro di Rust in questo capitolo, ma è stato utilizzato il macro println!, poiché l'uso di questi macro è abbastanza semplice, non è necessario padroneggiarli completamente per ora, possiamo imparare a utilizzare il macro panic! con lo stesso metodo.

fn main() {
    panic!("error occurred");
    println!("Hello, Rust");
}

Risultato dell'esecuzione:

thread 'main' panicked at 'error occurred', src\main.rs:3:5
note: esegui con la variabile d'ambiente `RUST_BACKTRACE=1` per visualizzare un backtrace.

Evidentemente, il programma non riesce a eseguire come previsto println!("Hello, Rust") ma si ferma quando viene chiamato il macro panic!.

Gli errori irreversibili causano inevitabilmente che il programma riceva un colpo mortale e si fermi.

Facciamo attenzione alle due righe di output dell'errore:

  • La prima riga esce la posizione chiamata dal macro panic! e le informazioni di errore generate.

  • La seconda riga è un messaggio di avviso, tradotto in cinese è "Esegui con la variabile d'ambiente `RUST_BACKTRACE=1` per visualizzare il backtrace". In seguito introdurremo il backtrace.

Proseguendo dall'esempio precedente, creiamo un terminale nuovo in VSCode:

Impostare le variabili d'ambiente nel terminale nuovo (i metodi possono variare da terminale a terminale, qui vengono descritti due metodi principali):

Se state utilizzando sistemi Windows 7 o superiori, il prompt dei comandi predefinito è Powershell, quindi utilizzate il seguente comando:

$env:RUST_BACKTRACE=1 ; cargo run

Se state utilizzando sistemi UNIX come Linux o macOS, di solito si utilizza il prompt dei comandi bash, quindi utilizzate il seguente comando:

RUST_BACKTRACE=1 cargo run

Poi, vedrete il seguente testo:

thread 'main' panicked at 'error occurred', src\main.rs:3:5
stack backtrace:
  ...
  11: greeting::main
             at .\src\main.rs:3
  ...

Il rollback è un altro modo di gestire gli errori irreversibili, che espande lo stack di esecuzione e visualizza tutte le informazioni, dopodiché il programma continua a eseguire. I puntini di sospensione (...) rappresentano una grande quantità di informazioni di output, tra cui possiamo trovare gli errori scatenati dal macro panic!:

Errore ripristabile

Questo concetto è molto simile all'eccezione nel linguaggio di programmazione Java. Infatti, in C, spesso configuriamo il valore di ritorno di una funzione come un intero per esprimere gli errori che la funzione incontra, in Rust utilizziamo l'enumerazione Result<T, E> come valore di ritorno per esprimere le eccezioni:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

In Rust, i valori di ritorno delle funzioni che possono generare eccezioni sono tutti di tipo Result. Ad esempio: quando cerchiamo di aprire un file:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => {
            println!("File opened successfully.");
        },
        Err(err) => {
            println!("Failed to open the file.");
        }
    }
}

Se il file hello.txt non esiste, verrà stampato: "Failed to open the file."

Naturalmente, la sintassi if let che abbiamo trattato nella sezione sull'enumerazione può semplificare il blocco match:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    if let Ok(file) = f {
        println!("File opened successfully.");
    } else {
        println!("Failed to open the file.");
    }
}

Se si desidera trattare un errore ripristabile come errore irreversibile, la classe Result fornisce due metodi: unwrap() e expect(message: &str):

use std::fs::File;
fn main() {
    let f1 = File::open("hello.txt").unwrap();
    let f2 = File::open("hello.txt").expect("Failed to open.");
}

Questo programma è equivalente a chiamare la macro panic! quando Result è Err. La differenza tra expect e panic! è che expect può inviare una stringa di errore specifica alla macro panic!.

Passaggio degli errori ripristinabili

Quello che ho detto prima è il modo di gestire gli errori ricevuti, ma cosa succede se scriviamo una funzione che vuole passingare gli errori quando si incontra un errore?

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}
fn main() {
    let r = f(10000);
    if let Ok(v) = r {
        println!("Ok: f(-1) = {}", v);
    } else {
        println!("Err");
    }
}

Risultato dell'esecuzione:

Ok: f(-1) = 10000

In questo programma, la funzione f è la fonte dell'errore, ora scriviamo una funzione g che passinga gli errori:

fn g(i: i32) -> Result<i32, bool> {
    let t = f(i);
    return match t {
        Ok(i) => Ok(i),
        Err(b) => Err(b)
    };
}

La funzione g passinga gli errori che potrebbe generare la funzione f (qui g è solo un esempio semplice, di solito la funzione che passinga gli errori contiene molte altre operazioni).

Scrivere così è un po' prolisso, in Rust si può aggiungere l'operatore ? dopo l'oggetto Result per passare direttamente gli Err dello stesso tipo:

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
    let t = f(i)?;
    Ok(t) // poiché t non è Err, t qui è già di tipo i32
}
fn main() {
    let r = g(10000);
    if let Ok(v) = r {
        println!("Ok: g(10000) = {}", v);
    } else {
        println!("Err");
    }
}

Risultato dell'esecuzione:

Ok: g(10000) = 10000

? simbolo ha l'effetto di estrarre direttamente il valore non eccezionale della classe Result, se c'è un'eccezione, restituisce direttamente Result con l'eccezione. Pertanto, il simbolo ? viene utilizzato solo per le funzioni con tipo di valore Result<T, E>, dove il tipo E deve essere identico al tipo E del Result gestito da ?.

metodo kind

Fino ad ora, sembra che Rust non abbia una sintassi simile al blocco try per risolvere direttamente tutti gli eccezioni che possono verificarsi in qualsiasi posizione, ma questo non significa che Rust non possa implementare: possiamo implementare il blocco try in una funzione indipendente, trasmettendo tutte le eccezioni per essere risolte. In realtà, questo è un metodo di programmazione che un programma ben progettato dovrebbe seguire: dovrebbe enfatizzare l'integrità delle funzioni indipendenti.

Ma così è necessario giudicare il tipo Err di Result, la funzione per ottenere il tipo Err è kind().

use std::io;
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
fn main() {
    let str_file = read_text_from_file("hello.txt");
    match str_file {
        Ok(s) => println!("{}", s),
        Err(e) => {
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!("No such file");
                },
                _ => {
                    println!("Cannot read the file");
                }
            }
        }
    }
}

Risultato dell'esecuzione:

File non trovato