Des erreurs récupérables avec Result
La plupart des erreurs ne sont pas assez graves au point d'arrêter complètement le programme. Parfois, lorsqu'une fonction échoue, c'est pour une raison que vous pouvez facilement comprendre et pour laquelle vous pouvez agir en conséquence. Par exemple, si vous essayez d'ouvrir un fichier et que l'opération échoue parce que le fichier n'existe pas, vous pourriez vouloir créer le fichier plutôt que d'arrêter le processus.
Souvenez-vous de la section “Gérer les erreurs potentielles avec le type
Result
” du chapitre 2 que l'énumération
Result
possède deux variantes, Ok
et Err
, comme ci-dessous :
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
Le T
et le E
sont des paramètres de type génériques : nous parlerons plus en
détail de la généricité au chapitre 10. Tout ce que vous avez besoin de savoir
pour le moment, c'est que T
représente le type de valeur imbriquée dans la
variante Ok
qui sera retournée dans le cas d'un succès, et E
représente le
type d'erreur imbriquée dans la variante Err
qui sera retournée dans le cas
d'un échec. Comme Result
a ces paramètres de type génériques, nous pouvons
utiliser le type Result
et les fonctions associées dans différentes
situations où la valeur de succès et la valeur d'erreur peuvent varier.
Utilisons une fonction qui retourne une valeur de type Result
car la fonction
peut échouer. Dans l'encart 9-3, nous essayons d'ouvrir un fichier :
Fichier : src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt"); }
Comment savons-nous que File::open
retourne un Result
? Nous pouvons
consulter la documentation de l'API de la bibliothèque
standard, ou nous
pouvons demander au compilateur ! Si nous appliquons à f
une annotation de
type dont nous savons qu'elle n'est pas le type de retour de la fonction et
que nous essayons ensuite de compiler le code, le compilateur va nous dire que
les types ne correspondent pas. Le message d'erreur va ensuite nous dire quel
est le type de f
. Essayons cela ! Nous savons que le type de retour de
File::open
n'est pas u32
, alors essayons de changer l'instruction let f
par ceci :
use std::fs::File;
fn main() {
let f: u32 = File::open("hello.txt");
}
Tenter de compiler ce code nous donne maintenant le résultat suivant :
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| --- ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
| |
| expected due to this
|
= note: expected type `u32`
found enum `Result<File, std::io::Error>`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `error-handling` due to previous error
Cela nous dit que le type de retour de la fonction File::open
est de la forme
Result<T, E>
. Le paramètre générique T
a été remplacé dans ce cas par le
type en cas de succès, std::fs::File
, qui permet d'interagir avec le fichier.
Le E
utilisé pour la valeur d'erreur est du type std::io::Error
.
Ce type de retour veut dire que l'appel à File::open
peut réussir et nous
retourner un manipulateur de fichier qui peut nous permettre de le lire ou d'y
écrire. L'utilisation de cette fonction peut aussi échouer : par exemple, si le
fichier n'existe pas, ou si nous n'avons pas le droit d'accéder au fichier. La
fonction File::open
doit avoir un moyen de nous dire si son utilisation a
réussi ou échoué et en même temps nous fournir soit le manipulateur de fichier,
soit des informations sur l'erreur. C'est exactement ces informations que
l'énumération Result
se charge de nous transmettre.
Dans le cas où File::open
réussit, la valeur que nous obtiendrons dans la
variable f
sera une instance de Ok
qui contiendra un manipulateur de
fichier. Dans le cas où cela échoue, la valeur dans f
sera une instance de
Err
qui contiendra plus d'information sur le type d'erreur qui a eu lieu.
Nous avons besoin d'ajouter différentes actions dans le code de l'encart 9-3 en
fonction de la valeur que File::open
retourne. L'encart 9-4 montre une façon
de gérer le Result
en utilisant un outil basique, l'expression match
que
nous avons vue au chapitre 6.
Fichier : src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(fichier) => fichier, Err(erreur) => panic!("Erreur d'ouverture du fichier : {:?}", erreur), }; }
Remarquez que, tout comme l'énumération Option
, l'énumération Result
et ses
variantes ont été importées par l'étape préliminaire, donc vous n'avez pas
besoin de préciser Result::
devant les variantes Ok
et Err
dans les
branches du match
.
Lorsque le résultat est Ok
, ce code va retourner la valeur fichier
contenue
dans la variante Ok
, et nous assignons ensuite cette valeur à la variable
f
. Après le match
, nous pourrons ensuite utiliser le manipulateur de
fichier pour lire ou écrire.
L'autre branche du bloc match
gère le cas où nous obtenons un Err
à l'appel
de File::open
. Dans cet exemple, nous avons choisi de faire appel à la macro
panic!
. S'il n'y a pas de fichier qui s'appelle hello.txt dans notre
répertoire actuel et que nous exécutons ce code, nous allons voir la sortie
suivante suite à l'appel de la macro panic!
:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at 'Erreur d'ouverture du fichier : Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Comme d'habitude, cette sortie nous explique avec précision ce qui s'est mal passé.
Gérer les différentes erreurs
Le code dans l'encart 9-4 va faire un panic!
peu importe la raison de l'échec
de File::open
. Cependant, nous voulons réagir différemment en fonction de
différents cas d'erreurs : si File::open
a échoué parce que le
fichier n'existe pas, nous voulons créer le fichier et retourner le manipulateur
de fichier pour ce nouveau fichier. Si File::open
échoue pour toute autre
raison, par exemple si nous n'avons pas l'autorisation d'ouvrir le fichier,
nous voulons quand même que le code lance un panic!
de la même manière qu'il
l'a fait dans l'encart 9-4. C'est pourquoi nous avons ajouté dans l'encart 9-5
une expression match
imbriquée :
Fichier : src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(fichier) => fichier,
Err(erreur) => match erreur.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Erreur de création du fichier : {:?}", e),
},
autre_erreur => {
panic!("Erreur d'ouverture du fichier : {:?}", autre_erreur)
}
},
};
}
La valeur de retour de File::open
logée dans la variante Err
est de type
io::Error
, qui est une structure fournie par la bibliothèque standard. Cette
structure a une méthode kind
que nous pouvons appeler pour obtenir une valeur
de type io::ErrorKind
. L'énumération io::ErrorKind
est fournie elle aussi
par la bibliothèque standard et a des variantes qui représentent les différents
types d'erreurs qui pourraient résulter d'une opération provenant du module
io
. La variante que nous voulons utiliser est ErrorKind::NotFound
, qui
indique que le fichier que nous essayons d'ouvrir n'existe pas encore. Donc nous
utilisons match
sur f
, mais nous avons dans celle-ci un autre match
sur
erreur.kind()
.
Nous souhaitons vérifier dans le match
interne si la valeur de retour de
error.kind()
est la variante NotFound
de l'énumération ErrorKind
. Si c'est
le cas, nous essayons de créer le fichier avec File::create
. Cependant, comme
File::create
peut aussi échouer, nous avons besoin d'une seconde branche dans
le match
interne. Lorsque le fichier ne peut pas être créé, un message
d'erreur différent est affiché. La seconde branche du match
principal reste
inchangée, donc le programme panique lorsqu'on rencontre une autre erreur que
l'absence de fichier.
D'autres solutions pour utiliser
match
avecResult<T, E>
Cela commence à faire beaucoup de
match
! L'expressionmatch
est très utile mais elle est aussi assez rudimentaire. Dans le chapitre 13, vous en apprendrez plus sur les fermetures, qui sont utilisées avec de nombreuses méthodes définies surResult<T, E>
. Ces méthodes peuvent s'avérer être plus concises que l'utilisation dematch
lorsque vous travaillez avec des valeursResult<T, E>
dans votre code.Par exemple, voici une autre manière d'écrire la même logique que celle dans l'encart 9-5 mais en utilisant les fermetures et la méthode
unwrap_or_else
:use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt").unwrap_or_else(|erreur| { if erreur.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|erreur| { panic!("Erreur de création du fichier : {:?}", erreur); }) } else { panic!("Erreur d'ouverture du fichier : {:?}", erreur); } }); }
Bien que ce code ait le même comportement que celui de l'encart 9-5, il ne contient aucune expression
match
et est plus facile à lire. Revenez sur cet exemple après avoir lu le chapitre 13, et renseignez-vous sur la méthodeunwrap_or_else
dans la documentation de la bibliothèque standard. De nombreuses méthodes de ce type peuvent clarifier de grosses expressionsmatch
imbriquées lorsque vous traitez les erreurs.
Raccourcis pour faire un panic lors d'une erreur : unwrap
et expect
L'utilisation de match
fonctionne assez bien, mais elle peut être un peu
verbeuse et ne communique pas forcément bien son intention. Le type
Result<T, E>
a de nombreuses méthodes qui lui ont été définies pour
différents cas. La méthode unwrap
est une méthode de raccourci implémentée
comme l'expression match
que nous avons écrite dans l'encart 9-4. Si la
valeur de Result
est la variante Ok
, unwrap
va retourner la valeur
contenue dans le Ok
. Si le Result
est la variante Err
, unwrap
va
appeler la macro panic!
pour nous. Voici un exemple de unwrap
en action :
Fichier : src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); }
Si nous exécutons ce code alors qu'il n'y a pas de fichier hello.txt, nous
allons voir un message d'erreur suite à l'appel à panic!
que la méthode
unwrap
a fait :
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4
De la même manière, la méthode expect
nous donne la possibilité de définir le
message d'erreur du panic!
. Utiliser expect
plutôt que unwrap
et lui
fournir un bon message d'erreur permet de mieux exprimer le problème et
faciliter la recherche de la source d'un panic. La syntaxe de expect
est la
suivante :
Fichier : src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt").expect("Échec à l'ouverture de hello.txt"); }
Nous utilisons expect
de la même manière que unwrap
: pour retourner le
manipulateur de fichier ou appeler la macro panic!
. Le message d'erreur
utilisé par expect
lors de son appel à panic!
sera le paramètre que nous
avons passé à expect
, plutôt que le message par défaut de panic!
qu'utilise
unwrap
. Voici ce que cela donne :
thread 'main' panicked at 'Échec à l'ouverture de hello.txt: Error { repr: Os {
code: 2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
Comme ce message d'erreur commence par le texte que nous avons précisé, Échec à l'ouverture de hello.txt
, ce sera plus facile de trouver là d'où provient ce
message d'erreur dans le code. Si nous utilisons unwrap
à plusieurs endroits,
cela peut prendre plus de temps de comprendre exactement quel unwrap
a causé
le panic, car tous les appels à unwrap
vont afficher le même message.
Propager les erreurs
Lorsqu'une fonction dont l'implémentation utilise quelque chose qui peut échouer, au lieu de gérer l'erreur directement dans cette fonction, vous pouvez retourner cette erreur au code qui l'appelle pour qu'il décide quoi faire. C'est ce que l'on appelle propager l'erreur et donne ainsi plus de contrôle au code qui appelle la fonction, dans lequel il peut y avoir plus d'informations ou d'instructions pour traiter l'erreur que dans le contexte de votre code.
Par exemple, l'encart 9-6 montre une fonction qui lit un pseudo à partir d'un fichier. Si ce fichier n'existe pas ou ne peut pas être lu, cette fonction va retourner ces erreurs au code qui a appelé la fonction.
Fichier : src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(fichier) => fichier, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } }
Cette fonction peut être écrite de façon plus concise, mais nous avons décidé de
commencer par faire un maximum de choses manuellement pour découvrir la gestion
d'erreurs ; mais à la fin, nous verrons comment raccourcir le code. Commençons
par regarder le type de retour de la fonction : Result<String, io::Error>
.
Cela signifie que la fonction retourne une valeur de type Result<T, E>
où le
paramètre générique T
a été remplacé par le type String
et le paramètre
générique E
a été remplacé par le type io::Error
. Si cette fonction réussit
sans problème, le code qui appellant va obtenir une valeur Ok
qui contient
une String
, le pseudo que cette fonction lit dans le fichier. Si cette
fonction rencontre un problème, le code qui appelle cette fonction va obtenir
une valeur Err
qui contient une instance de io::Error
qui donne plus
d'informations sur la raison du problème. Nous avons choisi io::Error
comme
type de retour de cette fonction parce qu'il se trouve que c'est le type
d'erreur de retour pour les deux opérations qui peuvent échouer que l'on utilise
dans le corps de cette fonction : la fonction File::open
et la méthode
read_to_string
.
Le corps de la fonction commence par appeler la fonction File::open
. Ensuite,
nous gérons la valeur du Result
avec un match
similaire au match
de
l'encart 9-4. Si le File::open
est un succès, le manipulateur de fichier dans
la variable fichier
du motif devient la valeur dans la variable mutable f
et la fonction continue son déroulement. Dans le cas d'un Err
, au lieu
d'appeler panic!
, nous utilisons return
pour sortir prématurément de toute
la fonction et en passant la valeur du File::open
, désormais dans la variable
e
, au code appelant comme valeur de retour de cette fonction.
Donc si nous avons un manipulateur de fichier dans f
, la fonction crée
ensuite une nouvelle String
dans la variable s
et nous appelons la méthode
read_to_string
sur le manipulateur de fichier f
pour extraire le contenu du
fichier dans s
. La méthode read_to_string
retourne aussi un Result
car
elle peut échouer, même si File::open
a réussi. Nous avons donc besoin d'un
nouveau match
pour gérer ce Result
: si read_to_string
réussit, alors
notre fonction a réussi, et nous retournons le pseudo que nous avons extrait du
fichier qui est maintenant intégré dans un Ok
, lui-même stocké dans s
. Si
read_to_string
échoue, nous retournons la valeur d'erreur de la même façon
que nous avons retourné la valeur d'erreur dans le match
qui gérait la valeur
de retour de File::open
. Cependant, nous n'avons pas besoin d'écrire
explicitement return
, car c'est la dernière expression de la fonction.
Le code qui appelle ce code va devoir ensuite gérer les cas où il récupère une
valeur Ok
qui contient un pseudo, ou une valeur Err
qui contient une
io::Error
. Il revient au code appelant de décider quoi faire avec ces
valeurs. Si le code appelant obtient une valeur Err
, il peut appeler panic!
et faire planter le programme, utiliser un pseudo par défaut, ou chercher le
pseudo autre part que dans ce fichier, par exemple. Nous n'avons pas assez
d'informations sur ce que le code appelant a l'intention de faire, donc nous
remontons toutes les informations de succès ou d'erreur pour qu'elles soient
gérées correctement.
Cette façon de propager les erreurs est si courante en Rust que Rust fournit
l'opérateur point d'interrogation ?
pour faciliter ceci.
Un raccourci pour propager les erreurs : l'opérateur ?
L'encart 9-7 montre une implémentation de lire_pseudo_depuis_fichier
qui a
les mêmes fonctionnalités que dans l'encart 9-6, mais cette implémentation
utilise l'opérateur point d'interrogation ?
:
Fichier : src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } }
Le ?
placé après une valeur Result
est conçu pour fonctionner presque de la
même manière que les expressions match
que nous avons définies pour gérer les
valeurs Result
dans l'encart 9-6. Si la valeur du Result
est un Ok
, la
valeur dans le Ok
sera retournée par cette expression et le programme
continuera. Si la valeur est un Err
, le Err
sera retourné par la fonction
comme si nous avions utilisé le mot-clé return
afin que la valeur d'erreur
soit propagée au code appelant.
Il y a une différence entre ce que fait l'expression match
de l'encart 9-6 et
ce que fait l'opérateur ?
: les valeurs d'erreurs sur lesquelles est utilisé
l'opérateur ?
passent par la fonction from
, définie dans le trait From
de
la bibliothèque standard, qui est utilisée pour convertir les erreurs d'un type
à un autre. Lorsque l'opérateur ?
appelle la fonction from
, le type d'erreur
reçu est converti dans le type d'erreur déclaré dans le type de retour de la
fonction concernée. C'est utile lorsqu'une fonction retourne un type d'erreur
qui peut couvrir tous les cas d'échec de la fonction, même si certaines de ses
parties peuvent échouer pour différentes raisons. À partir du moment qu'il y a
un impl From<AutreErreur>
sur ErreurRetournee
pour expliquer la conversion
dans la fonction from
du trait, l'opérateur ?
se charge d'appeler la
fonction from
automatiquement.
Dans le cas de l'encart 9-7, le ?
à la fin de l'appel à File::open
va
retourner la valeur à l'intérieur d'un Ok
à la variable f
. Si une erreur se
produit, l'opérateur ?
va quitter prématurément la fonction et retourner une
valeur Err
au code appelant. La même chose se produira au ?
à la fin de
l'appel à read_to_string
.
L'opérateur ?
allège l'écriture de code et facilite l'implémentation de la
fonction. Nous pouvons même encore plus réduire ce code en enchaînant
immédiatement les appels aux méthodes après le ?
comme dans l'encart 9-8 :
Fichier : src/main.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } }
Nous avons déplacé la création de la nouvelle String
dans s
au début de la
fonction ; cette partie n'a pas changé. Au lieu de créer la variable f
, nous
enchaînons directement l'appel à read_to_string
sur le résultat de
File::open("hello.txt")?
. Nous avons toujours le ?
à la fin de l'appel à
read_to_string
, et nous retournons toujours une valeur Ok
contenant le
pseudo dans s
lorsque File::open
et read_to_string
réussissent toutes les
deux plutôt que de retourner des erreurs. Cette fonctionnalité est toujours la
même que dans l'encart 9-6 et l'encart 9-7 ; c'est juste une façon différente et
plus ergonomique de l'écrire.
L'encart 9-9 nous montre comment encore plus raccourcir tout ceci en utilisant
fs::read_to_string
.
Fichier : src/main.rs
#![allow(unused)] fn main() { use std::fs; use std::io; fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
Récupérer le contenu d'un fichier dans une String
est une opération assez
courante, donc la bibliothèque standard fournit la fonction assez pratique
fs::read_to_string
, qui ouvre le fichier, crée une nouvelle String
, lit le
contenu du fichier, insère ce contenu dans cette String
, et la retourne.
Évidemment, l'utilisation de fs:read_to_string
ne nous offre pas l'occasion
d'expliquer toute la gestion des erreurs, donc nous avons d'abord utilisé la
manière la plus longue.
Où l'opérateur ?
peut être utilisé
L'opérateur ?
ne peut être utilisé uniquement que dans des fonctions dont le
type de retour compatible avec ce sur quoi le ?
est utilisé. C'est parce que
l'opérateur ?
est conçu pour retourner prématurémment une valeur de la
fonction, de la même manière que le faisait l'expression match
que nous avons
définie dans l'encart 9-6. Dans l'encart 9-6, le match
utilisait une valeur
de type Result
, et la branche de retour prématuré retournait une valeur de
type Err(e)
. Le type de retour de cette fonction doit être un Result
afin
d'être compatible avec ce return
.
Dans l'encart 9-10, découvrons l'erreur que nous allons obtenir si nous
utilisons l'opérateur ?
dans une fonction main
qui a un type de retour
incompatible avec le type de valeur sur laquelle nous utilisons ?
:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
Ce code ouvre un fichier, ce qui devrait échouer. L'opérateur ?
est placée
derrière la valeur de type Result
retournée par File::open
, mais cette
fonction main
a un type de retour ()
et non pas Result
. Lorsque nous
compilons ce code, nous obtenons le message d'erreur suivant :
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
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:36
|
3 | / fn main() {
4 | | let f = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error
Cette erreur explique que nous sommes autorisés à utiliser l'opérateur ?
uniquement dans une fonction qui retourne Result
, Option
, ou un autre type
qui implémente FromResidual
. Pour corriger l'erreur, vous avez deux choix. Le
premier est de changer le type de retour de votre fonction pour être compatible
avec la valeur avec lequel vous utilisez l'opérateur ?
, si vous pouvez le
faire. L'autre solution est d'utiliser un match
ou une des méthodes de
Result<T, E>
pour gérer le Result<T, E>
de la manière la plus appropriée.
Le message d'erreur indique également que ?
peut aussi être utilisé avec des
valeurs de type Option<T>
. Comme pour pouvoir utiliser ?
sur un Result
,
vous devez utiliser ?
sur Option
uniquement dans une fonction qui retourne
une Option
. Le comportement de l'opérateur ?
sur une Option<T>
est
identique au comportement sur un Result<T, E>
: si la valeur est None
, le
None
sera retourné prématurémment à la fonction dans laquelle il est utilisé.
Si la valeur est Some
, la valeur dans le Some
sera la valeur résultante de
l'expression et la fonction continuera son déroulement. L'encart 9-11 est un
exemple de fonction qui trouve le dernier caractère de la première ligne dans
le texte qu'on lui fournit :
fn dernier_caractere_de_la_premiere_ligne(texte: &str) -> Option<char> { texte.lines().next()?.chars().last() } fn main() { assert_eq!( dernier_caractere_de_la_premiere_ligne("Et bonjour\nComment ca va, aujourd'hui ?"), Some('r') ); assert_eq!(dernier_caractere_de_la_premiere_ligne(""), None); assert_eq!(dernier_caractere_de_la_premiere_ligne("\nsalut"), None); }
Cette fonction retourne un type Option<char>
car il est possible qu'il y ait
un caractère à cet endroit, mais il est aussi possible qu'il n'y soit pas. Ce
code prends l'argument texte
slice de chaîne de caractère et appelle sur elle
la méthode lines
, qui retourne un itérateur des lignes dans la chaîne. Comme
cette fonction veut traiter la première ligne, elle appelle next
sur
l'itérateur afin d'obtenir la première valeur de cet itérateur. Si texte
est
une chaîne vide, cet appel à next
va retourner None
, et dans ce cas nous
utilisons ?
pour arrêter le déroulement de la fonction et retourner None
.
Si texte
n'est pas une chaîne vide, next
va retourner une valeur de type
Some
contenant une slice de chaîne de caractères de la première ligne de
texte
.
Le ?
extrait la slice de la chaîne de caractères, et nous pouvons ainsi
appeller chars
sur cette slice de chaîne de caractères afin d'obtenir un
itérateur de ses caractères. Nous nous intéressons au dernier caractère de
cette première ligne, donc nous appelons last
pour retourner le dernier
élément dans l'itérateur. C'est une Option
car il est possible que la
première ligne soit une chaîne de caractères vide, par exemple si texte
commence par une ligne vide mais a des caractères sur les autres lignes, comme
par exemple "\nhi"
. Cependant, si il y a un caractère à la fin de la première
ligne, il sera retourné dans la variante Some
. L'opérateur ?
au millieu
nous donne un moyen concret d'exprimer cette logique, nous permettant
d'implémenter la fonction en une ligne. Si nous n'avions pas pu utiliser
l'opérateur ?
sur Option
, nous aurions dû implémenter cette logique en
utilisant plus d'appels à des méthodes ou des expressions match
.
Notez bien que vous pouvez utiliser l'opérateur ?
sur un Result
dans une
fonction qui retourne Result
, et vous pouvez utiliser l'opérateur ?
sur une
Option
dans une fonction qui retourne une Option
, mais vous ne pouvez pas
mélanger les deux. L'opérateur ?
ne va pas convertir un Result
en Option
et vice-versa ; dans ce cas, vous pouvez utiliser des méthodes comme la méthode
ok
sur Result
ou la méthode ok_or
sur Option
pour faire explicitement
la conversion.
Jusqu'ici, toutes les fonctions main
que nous avons utilisé retournent ()
.
La fonction main
est spéciale car c'est le point d'entrée et de sortie des
programmes exécutables, et il y a quelques limitations sur ce que peut être
le type de retour pour que les programmes se comportent correctement.
Heureusement, main
peut aussi retourner un Result<(), E>
. L'encart 9-12
reprend le code de l'encart 9-10 mais nous avons changé le type de retour du
main
pour être Result<(), Box<dyn Error>>
et nous avons ajouté la valeur de
retour Ok(())
à la fin. Ce code devrait maintenant pouvoir se compiler :
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
Le type Box<dyn Error>
est un objet trait, que nous verrons dans une
section du chapitre 17. Pour l'instant, vous
pouvez interpréter Box<dyn Error>
en “tout type d'erreur”. L'utilisation de
?
sur une valeur type Result
dans la fonction main
avec le type
Box<dyn Error>
est donc permise, car cela permet à n'importe quelle une
valeur de type Err
d'être retournée prématurément.
Lorsqu'une fonction main
retourne un Result<(), E>
, l'exécutable va
terminer son exécution avec une valeur de 0
si le main
retourne Ok(())
et
va se terminer avec une valeur différente de zéro si main
retourne une valeur
Err
. Les exécutables écrits en C retournent des entiers lorsqu'ils se
terminent : les programmes qui se terminent avec succès retournent l'entier
0
, et les programmes qui sont en erreur retournent un entier autre que 0
.
Rust retourne également des entiers avec des exécutables pour être compatible
avec cette convention.
La fonction main
peut retourner n'importe quel type qui implémente le trait
std::process::Termination
. Au moment de
l'écriture de ces mots, le trait Termination
est une fonctionnalité instable
seulement disponible avec la version expérimentale de Rust, donc vous ne pouvez
pas l'implémenter sur vos propres types avec la version stable de Rust, mais
vous pourrez peut-être le faire un jour !
Maintenant que nous avons vu les détails pour utiliser panic!
ou retourner
Result
, voyons maintenant comment choisir ce qu'il faut faire en fonction des
cas.