Error în Rust

Trait-ul comun al tuturor tipurilor de eroare: o eroare se poate afișa (Display, Debug) și poate avea o cauză.

Sintaxă
use std::error::Error;

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { None }
}

Ce face Error

std::error::Error este trait-ul pe care îl implementează erorile din biblioteca standard (io::Error, ParseIntError etc.) și pe care ar trebui să-l implementeze și erorile tale. Cere două lucruri: Debug (de obicei prin #[derive(Debug)]) și Display (mesajul pentru oameni, scris de tine). Metodele trait-ului au implementări implicite, deci un impl Error for EroareaMea {} gol este suficient.

Avantajul este că orice eroare poate fi pusă într-un Box<dyn Error>. O funcție care returnează Result<T, Box<dyn Error>> poate folosi ? pe erori de tipuri diferite: fiecare este convertită automat în Box<dyn Error>. Este varianta cea mai simplă pentru programe mici și pentru main.

Ce returnează

Trait-ul nu returnează nimic; vezi source pentru cauza unei erori.

Exemple

Exemplul 1: O eroare proprie

Enum-ul descrie cazurile de eroare; Display dă mesajul afișat, iar Error o face compatibilă cu restul bibliotecii.

use std::error::Error;
use std::fmt;

#[derive(Debug)]
enum EroareComanda {
    StocInsuficient { produs: String, disponibil: u32 },
    ProdusNecunoscut(String),
}

impl fmt::Display for EroareComanda {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            EroareComanda::StocInsuficient { produs, disponibil } => {
                write!(f, "mai sunt doar {disponibil} bucăți de {produs}")
            }
            EroareComanda::ProdusNecunoscut(p) => write!(f, "produsul „{p}” nu există"),
        }
    }
}

impl Error for EroareComanda {}

fn comanda(produs: &str, bucati: u32) -> Result<u32, EroareComanda> {
    match produs {
        "caiet" if bucati <= 5 => Ok(bucati * 4),
        "caiet" => Err(EroareComanda::StocInsuficient { produs: produs.to_string(), disponibil: 5 }),
        _ => Err(EroareComanda::ProdusNecunoscut(produs.to_string())),
    }
}

fn main() {
    for (p, n) in [("caiet", 3), ("caiet", 9), ("radieră", 1)] {
        match comanda(p, n) {
            Ok(total) => println!("Comandă OK: {total} lei"),
            Err(e) => println!("Eroare: {e}   (Debug: {e:?})"),
        }
    }
}

Rezultat

Comandă OK: 12 lei
Eroare: mai sunt doar 5 bucăți de caiet   (Debug: StocInsuficient { produs: "caiet", disponibil: 5 })
Eroare: produsul „radieră” nu există   (Debug: ProdusNecunoscut("radieră"))

Exemplul 2: Box pentru erori de tipuri diferite

? acceptă și io::Error, și ParseIntError, pentru că ambele se convertesc în Box<dyn Error>.

use std::error::Error;
use std::fs;

fn total_din_fisier(cale: &str) -> Result<i32, Box<dyn Error>> {
    let text = fs::read_to_string(cale)?;
    let mut total = 0;
    for linie in text.lines() {
        total += linie.trim().parse::<i32>()?;
    }
    Ok(total)
}

fn main() -> Result<(), Box<dyn Error>> {
    fs::write("bune.txt", "10\n20\n30\n")?;
    fs::write("rele.txt", "10\ndouăzeci\n")?;

    println!("bune.txt: {:?}", total_din_fisier("bune.txt"));
    for cale in ["rele.txt", "lipsa.txt"] {
        if let Err(e) = total_din_fisier(cale) {
            println!("{cale}: eroare: {e}");
        }
    }
    Ok(())
}

Rezultat

bune.txt: Ok(60)
rele.txt: eroare: invalid digit found in string
lipsa.txt: eroare: No such file or directory (os error 2)

Greșeli frecvente

  • Implementezi Error fără Display: nu compilează, pentru că Display este cerut de trait.
  • Pui în Display tot textul de la Debug. Mesajul Display este pentru utilizator: scurt, fără nume de tipuri, de obicei cu literă mică și fără punct la final.
  • Returnezi Box<dyn Error> dintr-o bibliotecă: apelantul nu mai poate face match pe cazurile de eroare. Pentru biblioteci, un enum propriu este mai util.

Comenzi înrudite

  • Error::source: Returnează eroarea care a cauzat eroarea curentă, dacă există, ca să poți afișa tot lanțul de cauze.
  • Result: Tipul pentru operații care pot eșua: Ok(valoare) la succes sau Err(eroare) cu motivul eșecului.
  • Result::map: map transformă valoarea din Ok și lasă eroarea neschimbată; map_err transformă eroarea și lasă valoarea neschimbată.
  • io::Error: Eroarea operațiilor de intrare/ieșire; kind() dă categoria (NotFound, PermissionDenied…), iar new() creează una proprie.
  • ParseIntError: Eroarea returnată când un text nu poate fi transformat în număr întreg; kind() spune motivul exact.