Errori Reversibili con Result
La maggior parte degli errori non è abbastanza grave da richiedere l’arresto completo del programma. A volte, quando una funzione fallisce, è per un motivo facilmente interpretabile e a cui è possibile rispondere. Ad esempio, se si tenta di aprire un file e l’operazione fallisce perché il file non esiste, potrebbe essere opportuno crearlo anziché terminare il processo.
Come menzionato in “Gestire i potenziali errori con
Result
” nel Capitolo 2 l’enum Result
è
definito come avente due varianti, Ok
ed Err
, come segue:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
ed E
sono parametri di type generico: parleremo dei generici più in
dettaglio nel Capitolo 10. Quello che devi sapere ora è che T
rappresenta il
type di valore che verrà restituito in caso di successo nella variante Ok
,
ed E
rappresenta il type di errore che verrà restituito in caso di
fallimento nella variante Err
. Poiché Result
ha questi parametri di type
generico, possiamo utilizzare il type Result
e le funzioni definite su di
esso in molte situazioni diverse in cui il valore di successo e il valore di
errore che vogliamo restituire potrebbero differire.
Chiamiamo una funzione che restituisca un valore Result
perché la funzione
potrebbe fallire. Nel Listato 9-3 proviamo ad aprire un file.
use std::fs::File; fn main() { let file_benvenuto_result = File::open("ciao.txt"); }
Il type restituito da File::open
è Result<T, E>
. Il parametro generico T
è stato riempito dall’implementazione di File::open
con il type del valore
di successo, std::fs::File
, che è un handle al file. Il type di E
utilizzato nel valore di errore è std::io::Error
. Questo type di ritorno
significa che la chiamata a File::open
potrebbe avere esito positivo e
restituire un handle al file da cui è possibile leggere o scrivere. La
chiamata di funzione potrebbe anche fallire: ad esempio, il file potrebbe non
esistere o potremmo non avere i permessi per accedervi. La funzione File::open
deve avere un modo per indicarci se è riuscita o meno e allo stesso tempo
fornirci l’handle al file o le informazioni sull’errore. Queste informazioni
sono esattamente ciò che l’enum Result
trasmette.
Nel caso in cui File::open
abbia esito positivo, il valore nella variabile
file_benvenuto_result
sarà un’istanza di Ok
che contiene un handle al
file. In caso di errore, il valore in file_benvenuto_result
sarà un’istanza di
Err
che contiene maggiori informazioni sul tipo di errore che si è verificato.
Dobbiamo aggiungere al codice del Listato 9-3 azioni diverse a seconda del
valore restituito da File::open
. Il Listato 9-4 mostra un modo per gestire il
risultato Result
utilizzando uno strumento di base, l’espressione match
di
cui abbiamo parlato nel Capitolo 6.
use std::fs::File; fn main() { let file_benvenuto_result = File::open("ciao.txt"); let file_benvenuto = match file_benvenuto_result { Ok(file) => file, Err(errore) => panic!("Errore nell'apertura del file: {errore:?}"), }; }
match
per gestire le varianti di Result
che potrebbero essere restituiteNota che, come l’enum Option
, l’enum Result
e le sue varianti sono state
introdotte nello scope dal preludio, quindi non è necessario specificare
Result::
prima delle varianti Ok
ed Err
nei rami di match
.
Quando il risultato è Ok
, questo codice restituirà il valore interno file
dalla variante Ok
, e quindi assegneremo il valore dell’handle al file alla
variabile file_benvenuto
. Dopo match
, possiamo utilizzare l’handle al file
per la lettura o la scrittura.
L’altro ramo di match
gestisce il caso in cui otteniamo un valore Err
da
File::open
. In questo esempio, abbiamo scelto di chiamare la macro panic!
.
Se non c’è alcun file denominato ciao.txt nella nostra cartella corrente ed
eseguiamo questo codice, vedremo il seguente output dalla macro panic!
:
$ cargo run
Compiling gestione_errore v0.1.0 (file:///progetti/gestione_errore)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
Running `target/debug/gestione_errore`
thread 'main' (6304) panicked at src/main.rs:8:24:
Errore nell'apertura del file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Come al solito, questo output ci dice esattamente cosa è andato storto.
Corrispondenza in Caso di Errori Diversi
Il codice nel Listato 9-4 genererà un panic!
indipendentemente dal motivo per
cui File::open
ha fallito. Tuttavia, vogliamo intraprendere azioni diverse per
diversi motivi di errore. Se File::open
ha fallito perché il file non esiste,
vogliamo crearlo e restituire l’handle al nuovo file. Se File::open
ha
fallito per qualsiasi altro motivo, ad esempio perché non avevamo
l’autorizzazione per aprire il file, vogliamo comunque che il codice generi un
panic!
come nel Listato 9-4. Per questo, aggiungiamo un’espressione match
interna, mostrata nel Listato 9-5.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file_benvenuto_result = File::open("ciao.txt");
let file_benvenuto = match file_benvenuto_result {
Ok(file) => file,
Err(errore) => match errore.kind() {
ErrorKind::NotFound => match File::create("ciao.txt") {
Ok(fc) => fc,
Err(e) => panic!("Errore nella creazione del file: {e:?}"),
},
_ => {
panic!("Errore nell'apertura del file: {errore:?}");
}
},
};
}
Il type del valore restituito da File::open
all’interno della variante Err
è io::Error
, una struct fornita dalla libreria standard. Questa struct ha
un metodo kind
che possiamo chiamare per ottenere un valore io::ErrorKind
.
L’enum io::ErrorKind
è fornito dalla libreria standard e ha varianti che
rappresentano i diversi tipi di errori che potrebbero verificarsi da
un’operazione io
. La variante che vogliamo utilizzare è ErrorKind::NotFound
,
che indica che il file che stiamo cercando di aprire non esiste ancora. Abbiamo
in pratica fatto un matching sia su file_benvenuto_result
sia, internamente,
sulla tipologia di errore in error.kind()
.
La condizione che vogliamo verificare nel matching interno è se il valore
restituito da error.kind()
è la variante NotFound
dell’enum ErrorKind
.
In tal caso, proviamo a creare il file con File::create
. Tuttavia, poiché
anche File::create
potrebbe fallire, abbiamo bisogno di un secondo ramo
nell’espressione match
interna. Quando il file non può essere creato, viene
visualizzato un messaggio di errore diverso. Il secondo ramo dell’espressione
match
esterna rimane invariato, quindi il programma va in panic in caso di
qualsiasi errore diverso dall’errore di file mancante.
Alternative all’Utilizzo di match
con Result<T, E>
Sono un sacco di match
! L’espressione match
è molto utile, ma anche molto
primitiva. Nel Capitolo 13, imparerai a conoscere le chiusure (closure), che
vengono utilizzate con molti dei metodi definiti in Result<T, E>
. Questi
metodi possono essere più concisi rispetto all’utilizzo di match
quando si
gestiscono i valori Result<T, E>
nel codice. Ad esempio, ecco un altro modo
per scrivere la stessa logica mostrata nel Listato 9-5, questa volta
utilizzando le chiusure e il metodo unwrap_or_else
:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file_benvenuto = File::open("ciao.txt").unwrap_or_else(|errore| {
if errore.kind() == ErrorKind::NotFound {
File::create("ciao.txt").unwrap_or_else(|errore| {
panic!("Errore nella creazione del file: {errore:?}");
})
} else {
panic!("Errore nell’apertura del file: {errore:?}");
}
});
}
Sebbene questo codice abbia lo stesso comportamento del Listato 9-5, non
contiene alcuna espressione match
ed è più chiaro da leggere. Torna a questo
esempio dopo aver letto il Capitolo 13 e cerca il metodo unwrap_or_else
nella documentazione della libreria standard. Molti altri di questi metodi
possono sostituire enormi espressioni annidate di match
quando si hanno
errori.
Scorciatoie per Panic in Caso di Errore
L’uso di match
funziona abbastanza bene, ma può essere un po’ prolisso e non
sempre comunica bene l’intento. Il type Result<T, E>
ha molti metodi utili
definiti al suo interno per svolgere varie attività più specifiche. Ad esempio
unwrap
è un metodo di scelta rapida implementato proprio come l’espressione
match
che abbiamo scritto nel Listato 9-4. Se il valore Result
è la variante
Ok
, unwrap
restituirà il valore all’interno di Ok
. Se Result
è la
variante Err
, unwrap
richiamerà la macro panic!
per noi. Ecco un esempio
di unwrap
in azione:
use std::fs::File; fn main() { let file_benvenuto = File::open("ciao.txt").unwrap(); }
Se eseguiamo questo codice senza un file ciao.txt, vedremo un messaggio di
errore dalla chiamata panic!
effettuata dal metodo unwrap
:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Analogamente, il metodo expect
ci consente anche di scegliere il messaggio di
errore panic!
. Usare expect
invece di unwrap
e fornire messaggi di errore
efficaci può trasmettere le proprie intenzioni e facilitare l’individuazione
della fonte di un errore. La sintassi di expect
è la seguente:
use std::fs::File; fn main() { let file_benvenuto = File::open("ciao.txt") .expect("ciao.txt dovrebbe essere presente in questo progetto"); }
Usiamo expect
allo stesso modo di unwrap
: per restituire l’handle al file
o chiamare la macro panic!
. Il messaggio di errore utilizzato da expect
nella sua chiamata a panic!
sarà il parametro che passeremo a expect
,
anziché il messaggio predefinito panic!
utilizzato da unwrap
. Ecco come
appare:
thread 'main' panicked at src/main.rs:5:10:
ciao.txt dovrebbe essere presente in questo progetto: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Nel codice ottimizzato per il rilascio, la maggior parte dei Rustacean sceglie
expect
invece di unwrap
per fornire più contesto sul motivo per cui ci si
aspetta che l’operazione è fallita. In questo modo, se le tue ipotesi dovessero
rivelarsi sbagliate, avrai più informazioni da utilizzare per il debug.
Propagazione degli Errori
Quando l’implementazione di una funzione richiama qualcosa che potrebbe non funzionare, invece di gestire l’errore all’interno della funzione stessa, è possibile restituirlo al codice chiamante in modo che possa decidere cosa fare. Questa operazione è nota come propagazione dell’errore e conferisce maggiore controllo al codice chiamante, dove potrebbero esserci più informazioni o logica che determinano come gestire l’errore rispetto a quelle disponibili nel contesto del codice.
Ad esempio, il Listato 9-6 mostra una funzione che legge un nome utente da un file. Se il file non esiste o non può essere letto, questa funzione restituirà gli errori al codice che ha chiamato la funzione.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn leggi_nomeutente_dal_file() -> Result<String, io::Error> { let nomeutente_file_result = File::open("ciao.txt"); let mut nomeutente_file = match nomeutente_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut nomeutente = String::new(); match nomeutente_file.read_to_string(&mut nomeutente) { Ok(_) => Ok(nomeutente), Err(e) => Err(e), } } }
match
Questa funzione può essere scritta in un modo molto più breve, ma inizieremo
eseguendo gran parte del processo manualmente per esplorare la gestione degli
errori; alla fine, mostreremo il modo più breve. Per prima cosa diamo
un’occhiata al type di ritorno della funzione: Result<String, io::Error>
.
Ciò significa che la funzione restituisce un valore del type Result<T, E>
,
dove il parametro generico T
è stato riempito con il type concreto String
e il type generico E
è stato riempito con il type concreto io::Error
.
Se questa funzione ha esito positivo senza problemi, il codice che la chiama
riceverà un valore Ok
che contiene una String
, ovvero il nomeutente
che
questa funzione ha letto dal file. Se questa funzione riscontra problemi, il
codice chiamante riceverà un valore Err
che contiene un’istanza di io::Error
contenente maggiori informazioni sulla causa del problema. Abbiamo scelto
io::Error
come type di ritorno di questa funzione perché è il type del
valore di errore restituito da entrambe le operazioni che stiamo chiamando nel
corpo di questa funzione che potrebbero fallire: la funzione File::open
e il
metodo read_to_string
.
Il corpo della funzione inizia con la chiamata alla funzione File::open
.
Quindi gestiamo il valore Result
con un match
simile a quello nel Listato
9-4. Se File::open
ha esito positivo, l’handle al file nella variabile
pattern file
diventa il valore nella variabile mutabile nomeutente_file
e
la funzione continua. Nel caso Err
, invece di chiamare panic!
, utilizziamo
la parola chiave return
per uscire completamente dalla funzione e passare il
valore di errore da File::open
, ora nella variabile pattern e
, al codice
chiamante come valore di errore di questa funzione.
Quindi, se abbiamo un handle al file in nomeutente_file
, la funzione crea
una nuova String
nella variabile nomeutente
e chiama il metodo
read_to_string
sull’handle al file in nomeutente_file
per leggere il
contenuto del file in nomeutente
. Anche il metodo read_to_string
restituisce
un Result
perché potrebbe fallire, anche se File::open
ha avuto esito
positivo. Abbiamo quindi bisogno di un altro match
per gestire quel Result
:
se read_to_string
ha esito positivo, la nostra funzione ha avuto successo e
restituiamo il nome utente dal file che ora si trova in nomeutente
incapsulato
in un Ok
. Se read_to_string
fallisce, restituiamo il valore di errore nello
stesso modo in cui abbiamo restituito il valore di errore nel match
che
gestiva il valore di ritorno di File::open
. Tuttavia, non è necessario
specificare esplicitamente return
, perché questa è l’ultima espressione nella
funzione.
Il codice chiamante gestirà quindi l’ottenimento di un valore Ok
che contiene
un nome utente o di un valore Err
che contiene un io::Error
. Spetta al
codice chiamante decidere cosa fare con questi valori. Se il codice chiamante
riceve un valore Err
, potrebbe chiamare panic!
e mandare in crash il
programma, utilizzare un nome utente predefinito o cercare il nome utente da una
posizione diversa da un file, ad esempio. Non abbiamo informazioni sufficienti
su cosa stia effettivamente cercando di fare il codice chiamante, quindi
propaghiamo tutte le informazioni di successo o errore verso l’alto affinché
possa gestirle in modo appropriato.
Questo schema di propagazione degli errori è così comune in Rust che Rust
fornisce l’operatore punto interrogativo ?
per semplificare la procedura.
L’Operatore Scorciatoia ?
Il Listato 9-7 mostra un’implementazione di leggi_nomeutente_dal_file
che ha
la stessa funzionalità del Listato 9-6, ma questa implementazione utilizza
l’operatore ?
.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn leggi_nomeutente_dal_file() -> Result<String, io::Error> { let mut nomeutente_file = File::open("ciao.txt")?; let mut nomeutente = String::new(); nomeutente_file.read_to_string(&mut nomeutente)?; Ok(nomeutente) } }
?
Il ?
inserito dopo un valore Result
è definito per funzionare quasi allo
stesso modo delle espressioni match
che abbiamo definito per gestire i valori
Result
nel Listato 9-6. Se il valore di Result
è Ok
, il valore all’interno
di Ok
verrà restituito da questa espressione e il programma continuerà. Se il
valore è Err
, Err
verrà restituito dall’intera funzione come se avessimo
utilizzato la parola chiave return
, quindi il valore di errore viene propagato
al codice chiamante.
C’è una differenza tra ciò che fa l’espressione match
del Listato 9-6 e ciò
che fa l’operatore ?
: i valori di errore che hanno l’operatore ?
chiamato su
di essi passano attraverso la funzione from
, definita nel trait From
nella
libreria standard, che viene utilizzata per convertire i valori da un type
all’altro. Quando l’operatore ?
chiama la funzione from
, il type di errore
ricevuto viene convertito nel type di errore definito nel type di ritorno
della funzione corrente. Questo è utile quando una funzione restituisce un
type di errore per rappresentare tutti i modi in cui la funzione potrebbe
fallire, anche se alcune parti potrebbero fallire per molti motivi diversi.
Ad esempio, potremmo modificare la funzione leggi_nomeutente_dal_file
nel
Listato 9-7 per restituire un type personalizzato di errore denominato
NostroErrore
da noi definito. Se definiamo anche impl From<io::Error> for NostroErrore
per costruire un’istanza di NostroErrore
partendo da un
io::Error
, l’operatore ?
chiamato nel corpo di leggi_nomeutente_dal_file
chiamerà from
e convertirà i type di errore senza bisogno di aggiungere
altro codice alla funzione.
Nel contesto del Listato 9-7, ?
alla fine della chiamata File::open
restituirà il valore all’interno di un Ok
alla variabile nomeutente_file
. Se
si verifica un errore, l’operatore ?
interromperà l’intera funzione in
anticipo e ritornerà un valore Err
al codice chiamante. Lo stesso vale per ?
alla fine della chiamata read_to_string
.
L’operatore ?
elimina gran parte del codice superfluo e semplifica
l’implementazione di questa funzione. Potremmo anche accorciare ulteriormente
questo codice concatenando le chiamate ai metodi subito dopo ?
, come mostrato
nel Listato 9-8.
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn leggi_nomeutente_dal_file() -> Result<String, io::Error> { let mut nomeutente = String::new(); File::open("ciao.txt")?.read_to_string(&mut nomeutente)?; Ok(nomeutente) } }
?
Abbiamo spostato la creazione della nuova String
in nomeutente
all’inizio
della funzione; questa parte non è cambiata. Invece di creare una variabile
nomeutente_file
, abbiamo concatenato la chiamata a read_to_string
direttamente al risultato di File::open("hello.txt")?
. Abbiamo ancora un ?
alla fine della chiamata a read_to_string
e continuiamo a restituire un valore
Ok
contenente nomeutente
quando sia File::open
che read_to_string
hanno
esito positivo, anziché restituire errori. La funzionalità è ancora la stessa
dei Listati 9-6 e 9-7; questo è solo un modo diverso e più stringato di
scriverlo.
Il Listato 9-9 mostra un modo per renderlo ancora più breve usando
fs::read_to_string
.
#![allow(unused)] fn main() { use std::fs; use std::io; fn leggi_nomeutente_dal_file() -> Result<String, io::Error> { fs::read_to_string("ciao.txt") } }
fs::read_to_string
invece di aprire e poi leggere il fileLeggere un file in una stringa è un’operazione abbastanza comune, quindi la
libreria standard fornisce la comoda funzione fs::read_to_string
che apre il
file, crea una nuova String
, ne legge il contenuto, lo inserisce in quella
String
e la restituisce. Ovviamente, usare fs::read_to_string
non ci dà
l’opportunità di spiegare tutta la gestione degli errori, quindi l’abbiamo fatto
prima nel modo più lungo.
Dove Usare l’Operatore ?
L’operatore ?
può essere utilizzato solo in funzioni il cui type di ritorno
è compatibile con il valore su cui viene utilizzato ?
. Questo perché
l’operatore ?
è definito per eseguire una restituzione anticipata di un valore
dalla funzione, allo stesso modo dell’espressione match
che abbiamo definito
nel Listato 9-6. Nel Listato 9-6, la funzione match
utilizzava un valore
Result
e il ramo di ritorno anticipato restituiva un valore Err(e)
. Il
type di ritorno della funzione deve essere Result
in modo che sia
compatibile con questo return
.
Nel Listato 9-10, esaminiamo l’errore che otterremo se utilizziamo l’operatore
?
in una funzione main
con un type di ritorno incompatibile con il type
del valore su cui utilizziamo ?
.
use std::fs::File;
fn main() {
let file_benvenuto = File::open("ciao.txt")?;
}
?
nella funzione main
che restituisce ()
non verrà compilatoQuesto codice apre un file, che potrebbe non funzionare. L’operatore ?
segue
il valore Result
restituito da File::open
, ma questa funzione main
ha come
type di ritorno ()
, non Result
. Quando compiliamo questo codice, otteniamo
il seguente messaggio di errore:
$ cargo run
Compiling gestione_errore v0.1.0 (file:///progetti/gestione_errore)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let file_benvenuto = File::open("ciao.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let file_benvenuto = File::open("ciao.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gestione_errore` (bin "gestione_errore") due to 1 previous error
Questo errore indica che possiamo utilizzare l’operatore ?
solo in una
funzione che restituisce Result
, Option
o un altro type che implementa
FromResidual
.
Per correggere l’errore, hai due possibilità. Una è modificare il type di
ritorno della funzione in modo che sia compatibile con il valore su cui stai
utilizzando l’operatore ?
, a condizione che non ci siano restrizioni che lo
impediscano. L’altra possibilità è utilizzare un match
o uno dei metodi
Result<T, E>
per gestire Result<T, E>
nel modo più appropriato.
Il messaggio di errore indica anche che ?
può essere utilizzato anche con i
valori Option<T>
. Come per l’utilizzo di ?
su Result
, è possibile
utilizzare ?
solo su Option
in una funzione che restituisce Option
. Il
comportamento dell’operatore ?
quando viene chiamato su Option<T>
è simile
al suo comportamento quando viene chiamato su Result<T, E>
: se il valore è
None
, None
verrà restituito in anticipo dalla funzione in quel punto. Se il
valore è Some
, il valore all’interno di Some
è il valore risultante
dell’espressione e la funzione continua. Il Listato 9-11 contiene un esempio di
una funzione che trova l’ultimo carattere della prima riga del testo dato.
fn ultimo_char_della_prima_riga(testo: &str) -> Option<char> { testo.lines().next()?.chars().last() } fn main() { assert_eq!( ultimo_char_della_prima_riga("Hello, world\nCome stai oggi?"), Some('d') ); assert_eq!(ultimo_char_della_prima_riga(""), None); assert_eq!(ultimo_char_della_prima_riga("\nhi"), None); }
?
su un valore Option<T>
Questa funzione restituisce Option<char>
perché è possibile che ci sia un
carattere, ma è anche possibile che non ci sia. Questo codice prende l’argomento
slice testo
e chiama il metodo lines
su di esso, che restituisce un
iteratore sulle righe della stringa. Poiché questa funzione vuole esaminare la
prima riga, chiama next
sull’iteratore per ottenere il primo valore
dall’iteratore. Se testo
è una stringa vuota, questa chiamata a next
restituirà None
, nel qual caso usiamo ?
per fermarci e restituire None
da
ultimo_char_della_prima_riga
. Se testo
non è una stringa vuota, next
restituirà un valore Some
contenente una slice della prima riga di testo
.
?
estrae la slice e possiamo chiamare chars
su quella slice per ottenere
un iteratore dei suoi caratteri. Siamo interessati all’ultimo carattere in
questa prima riga, quindi chiamiamo last
per restituire l’ultimo elemento
nell’iteratore. Questo è un’Option
perché è possibile che la prima riga sia
una stringa vuota; ad esempio, se testo
inizia con una riga vuota ma ha
caratteri su altre righe, come in "\nhi"
. Tuttavia, se c’è un ultimo carattere
sulla prima riga, verrà restituito nella variante Some
. L’operatore ?
al
centro ci fornisce un modo conciso per esprimere questa logica, permettendoci di
implementare la funzione in una sola riga. Se non potessimo usare l’operatore
?
su Option
, dovremmo implementare questa logica utilizzando più chiamate di
metodo o un’espressione match
.
Nota che è possibile utilizzare l’operatore ?
su un Result
in una funzione
che restituisce Result
, e si può utilizzare l’operatore ?
su un Option
in
una funzione che restituisce Option
, ma non è possibile combinare le due.
L’operatore ?
non convertirà automaticamente un Result
in un Option
o
viceversa; in questi casi, è possibile utilizzare il metodo ok
su Result
o
il metodo ok_or
su Option
per eseguire la conversione in modo esplicito.
Finora, tutte le funzioni main
che abbiamo utilizzato restituiscono ()
. La
funzione main
è speciale perché è il punto di ingresso e di uscita di un
programma eseguibile, e ci sono delle restrizioni sul type di ritorno che può
essere usato affinché il programma si comporti come previsto.
Fortunatamente, main
può anche restituire Result<(), E>
. Il Listato 9-12
contiene il codice del Listato 9-10, ma abbiamo modificato il type di ritorno
di main
in Result<(), Box<dyn Error>>
e aggiunto un valore di ritorno
Ok(())
alla fine. Questo codice ora verrà compilato.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let file_benvenuto = File::open("ciao.txt")?;
Ok(())
}
main
in modo che restituisca Result<(), E>
è possibile utilizzare l’operatore ?
sui valori Result
Il type Box<dyn Error>
è un oggetto trait, di cui parleremo in “Usare
gli Oggetti Trait per Astrarre Comportamenti Condivisi” nel Capitolo 18. Per ora, puoi leggere Box<dyn Error>
come
“qualsiasi type di errore”. L’utilizzo di ?
su un valore Result
in una
funzione main
con il type di errore Box<dyn Error>
è consentito perché
consente la restituzione anticipata di qualsiasi valore Err
. Anche se il corpo
di questa funzione main
restituirà sempre e solo errori di type
std::io::Error
, specificando Box<dyn Error>
, questa firma continuerà a
essere corretta anche se al corpo di main
viene aggiunto altro codice che
restituisce altri errori.
Quando una funzione main
restituisce Result<(), E>
, l’eseguibile terminerà
restituendo un valore di 0
se main
restituisce Ok(())
e uscirà con un
valore diverso da zero se main
restituisce un valore Err
. Gli eseguibili
scritti in C restituiscono integer quando terminano: i programmi che terminano
correttamente restituiscono l’integer 0
, e i programmi che generano un
errore restituiscono un integer diverso da 0
. Anche Rust restituisce
integer dagli eseguibili per essere compatibile con questa convenzione.
La funzione main
può restituire qualsiasi type che implementi il trait
std::process::Termination
, contenente una
funzione report
che restituisce un ExitCode
. Consulta la documentazione
della libreria standard per maggiori informazioni sull’implementazione del
trait Termination
per i tuoi type.
Ora che abbiamo discusso i dettagli della chiamata a panic!
o della
restituzione di Result
, discuteremo di come decidere in quali casi sia più
appropriato usare l’uno o l’altro.