00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Result in Rust

Il tipo Result<T, E> e lo strumento principale di Rust per la gestione degli errori recuperabili. Mentre Option rappresenta la presenza o assenza di un valore, Result rappresenta il successo o il fallimento di un’operazione, portando con se informazioni sull’errore.

Ok(T) e Err(E)

Result<T, E> e un enum con due varianti:

// Definizione nella libreria standard
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

fn main() {
    let successo: Result<i32, String> = Ok(42);
    let fallimento: Result<i32, String> = Err(String::from("qualcosa e andato storto"));

    println!("Successo: {:?}", successo);    // Ok(42)
    println!("Fallimento: {:?}", fallimento); // Err("qualcosa e andato storto")
}

Il Pattern di Gestione degli Errori

Il pattern piu comune e restituire Result da funzioni che possono fallire:

use std::num::ParseIntError;

fn analizza_numero(testo: &str) -> Result<i32, ParseIntError> {
    testo.trim().parse::<i32>()
}

fn main() {
    match analizza_numero("42") {
        Ok(n) => println!("Numero: {}", n),
        Err(e) => println!("Errore di parsing: {}", e),
    }

    match analizza_numero("abc") {
        Ok(n) => println!("Numero: {}", n),
        Err(e) => println!("Errore di parsing: {}", e),
    }
}

unwrap e expect

Come per Option, unwrap e expect estraggono il valore da Ok o causano un panic su Err:

fn main() {
    let ok: Result<i32, &str> = Ok(10);
    let err: Result<i32, &str> = Err("errore");

    // unwrap: estrae il valore o va in panic
    println!("Valore: {}", ok.unwrap()); // 10

    // expect: panic con messaggio personalizzato
    // err.expect("Operazione fallita!"); // PANIC con messaggio

    // Controllo sicuro
    println!("E Ok: {}", ok.is_ok());   // true
    println!("E Err: {}", err.is_err()); // true
}

Pattern Matching con Result

Il match e il modo piu esplicito per gestire entrambi i casi:

use std::fs;

fn leggi_configurazione(percorso: &str) -> Result<String, String> {
    fs::read_to_string(percorso)
        .map_err(|e| format!("Impossibile leggere {}: {}", percorso, e))
}

fn main() {
    match leggi_configurazione("config.toml") {
        Ok(contenuto) => {
            println!("Configurazione caricata:");
            println!("{}", contenuto);
        }
        Err(errore) => {
            eprintln!("Errore: {}", errore);
        }
    }
}

Metodi Combinatori: map, map_err, and_then

I metodi combinatori permettono di trasformare Result in modo funzionale:

fn main() {
    let numero: Result<i32, String> = Ok(5);
    let errore: Result<i32, String> = Err(String::from("errore"));

    // map: trasforma il valore Ok
    let doppio = numero.map(|n| n * 2);
    println!("Doppio: {:?}", doppio); // Ok(10)

    // map_err: trasforma il valore Err
    let err_formattato = errore.map_err(|e| format!("ERRORE: {}", e));
    println!("{:?}", err_formattato); // Err("ERRORE: errore")

    // and_then (flatmap): concatena operazioni che possono fallire
    let risultato = Ok("42")
        .and_then(|s: &str| {
            s.parse::<i32>().map_err(|e| e.to_string())
        })
        .and_then(|n| {
            if n > 0 {
                Ok(n * 2)
            } else {
                Err(String::from("Il numero deve essere positivo"))
            }
        });
    println!("Risultato: {:?}", risultato); // Ok(84)

    // unwrap_or e unwrap_or_else
    let val = errore.unwrap_or(0);
    println!("Con default: {}", val); // 0

    let val = errore.unwrap_or_else(|e| {
        eprintln!("Errore ignorato: {}", e);
        -1
    });
    println!("Con default calcolato: {}", val); // -1
}

L’Operatore ?

L’operatore ? e il modo piu idiomatico per propagare gli errori. Posto dopo un’espressione Result, estrae il valore Ok oppure restituisce immediatamente l’errore Err dalla funzione corrente.

use std::fs;
use std::io;

fn leggi_nome_utente(percorso: &str) -> Result<String, io::Error> {
    let contenuto = fs::read_to_string(percorso)?;
    Ok(contenuto.trim().to_string())
}

// Concatenare piu operazioni fallibili
fn prima_riga(percorso: &str) -> Result<String, io::Error> {
    let contenuto = fs::read_to_string(percorso)?;
    let riga = contenuto
        .lines()
        .next()
        .ok_or(io::Error::new(io::ErrorKind::InvalidData, "File vuoto"))?;
    Ok(riga.to_string())
}

fn main() {
    match leggi_nome_utente("utente.txt") {
        Ok(nome) => println!("Nome utente: {}", nome),
        Err(e) => eprintln!("Errore lettura: {}", e),
    }
}

L’operatore ? richiede che il tipo di errore sia compatibile. Se i tipi sono diversi, Rust usa il trait From per la conversione automatica.

Conversione tra Option e Result

Rust fornisce metodi per convertire tra Option e Result:

fn main() {
    // Option -> Result
    let opzione: Option<i32> = Some(42);
    let risultato: Result<i32, &str> = opzione.ok_or("valore assente");
    println!("{:?}", risultato); // Ok(42)

    let niente: Option<i32> = None;
    let risultato: Result<i32, &str> = niente.ok_or("valore assente");
    println!("{:?}", risultato); // Err("valore assente")

    // ok_or_else con closure (lazy evaluation)
    let risultato = niente.ok_or_else(|| {
        format!("Errore generato alle {:?}", std::time::SystemTime::now())
    });
    println!("{:?}", risultato);

    // Result -> Option
    let ok: Result<i32, &str> = Ok(10);
    let opzione = ok.ok(); // Some(10)
    println!("{:?}", opzione);

    let err: Result<i32, &str> = Err("errore");
    let opzione = err.ok(); // None
    println!("{:?}", opzione);

    // Anche per il lato errore
    let errore_opzione = err.err(); // Some("errore")
    println!("{:?}", errore_opzione);
}

Esempio Pratico: Parsing di Configurazione

use std::collections::HashMap;

#[derive(Debug)]
struct Config {
    host: String,
    porta: u16,
    max_connessioni: u32,
}

fn analizza_config(input: &str) -> Result<Config, String> {
    let mut mappa: HashMap<&str, &str> = HashMap::new();

    for riga in input.lines() {
        let parti: Vec<&str> = riga.splitn(2, '=').collect();
        if parti.len() == 2 {
            mappa.insert(parti[0].trim(), parti[1].trim());
        }
    }

    let host = mappa.get("host")
        .ok_or("Campo 'host' mancante")?
        .to_string();

    let porta = mappa.get("porta")
        .ok_or("Campo 'porta' mancante")?
        .parse::<u16>()
        .map_err(|e| format!("Porta non valida: {}", e))?;

    let max_connessioni = mappa.get("max_connessioni")
        .ok_or("Campo 'max_connessioni' mancante")?
        .parse::<u32>()
        .map_err(|e| format!("max_connessioni non valido: {}", e))?;

    Ok(Config { host, porta, max_connessioni })
}

fn main() {
    let input = "host = localhost\nporta = 8080\nmax_connessioni = 100";
    match analizza_config(input) {
        Ok(config) => println!("Config: {:?}", config),
        Err(e) => eprintln!("Errore: {}", e),
    }
}

Conclusione

Result<T, E> e il fondamento della gestione degli errori in Rust. A differenza delle eccezioni di altri linguaggi, gli errori in Rust sono valori espliciti che devono essere gestiti. L’operatore ? rende la propagazione degli errori concisa e leggibile, i metodi combinatori come map, map_err e and_then permettono trasformazioni eleganti, e la conversione tra Option e Result offre grande flessibilita. La regola fondamentale: usa Result per errori recuperabili, panic! solo per situazioni irrecuperabili.