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 :
use std::fs::File;
fn main() {
let resultat_fichier_salut = File::open("hello.txt");
}
Le type de retour de File::open est un Result<T, E>. Le paramètre générique T a été renseigné par l’implémentation deFile::open avec le type de la valeur qui a réussi, std::fs::File, qui est un descripteur de fichier. Le E utilisé pour la valeur d’erreur est du type std::io::Error. Ce type de retour signifie que l’appel à File::open peut réussir et renvoyer un descripteur de fichier qui peut nous permettre de le lire ou d’y écrire. L’appel de fonction peut également échouer : par exemple, le fichier peut ne pas exister, ou nous pourrions ne pas avoir les autorisations requises pour 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. Ce sont 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 resultat_fichier_salut sera une instance de Ok qui contiendra un manipulateur de fichier. Dans le cas où cela échoue, la valeur dans resultat_fichier_salut 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.
use std::fs::File;
fn main() {
let resultat_fichier_salut = File::open("hello.txt");
let resultat_fichier_salut = match resultat_fichier_salut {
Ok(fichier) => fichier,
Err(erreur) => panic!("Erreur d'ouverture du fichier : {erreur:?}"),
};
}
match expression to handle the Result variants that might be returnedRemarquez 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 resultat_fichier_salut. 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` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Erreur d'ouverture du fichier : Os { code: 2, kind: NotFound, message: "No such file or directory" }
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 :
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let resultat_fichier_salut = File::open("hello.txt");
let resultat_fichier_salut = match resultat_fichier_salut {
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:?}"),
},
_ => {
panic!("Erreur d'ouverture du fichier : {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 resultat_fichier_salut, 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 avec Result<T, E>
Cela commence à faire beaucoup de match ! L’expression match 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 sur Result<T, E>. Ces méthodes peuvent s’avérer être plus concises que l’utilisation de match lorsque vous travaillez avec des valeurs Result<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, cette fois en utilisant les fermetures et la méthode unwrap_or_else :
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let resultat_fichier_salut = 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éthode unwrap_or_else dans la documentation de la bibliothèque standard. De nombreuses méthodes de ce type peuvent clarifier de grosses expressions match 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 :
use std::fs::File;
fn main() {
let fichier_salut = 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 src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
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 :
use std::fs::File;
fn main() {
let fichier_salut = File::open("hello.txt")
.expect("hello.txt doit être inclus dans ce projet");
}
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 src/main.rs:5:10:
hello.txt doit être inclus dans ce projet: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Dans du code destiné à de la production, la plupart des Rustacés choisissent expect plutôt que unwrap et fournissent davantage de d’informations concernant les raisons pour lesquelles l’opération est supposée toujours aboutir. De la sorte, si vos hypothèses s’avèrent erronées, vous avez plus d’informations à utiliser pour le débogage.
Propager les erreurs
Lorsque l’implémentation d’une fonction 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 que ce dernier décide que 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.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> {
let resultat_fichier_pseudo = File::open("hello.txt");
let mut fichier_pseudo = match resultat_fichier_pseudo {
Ok(fichier) => fichier,
Err(e) => return Err(e),
};
let mut pseudo = String::new();
match fichier_pseudo.read_to_string(&mut pseudo) {
Ok(_) => Ok(pseudo),
Err(e) => Err(e),
}
}
}
matchCette 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 ; à 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 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 fichier_pseudo 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 fichier_pseudo, la fonction crée ensuite une nouvelle String dans la variable pseudo et nous appelons la méthode read_to_string sur le manipulateur de fichier fichier_pseudo pour extraire le contenu du fichier dans pseudo. 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 pseudo. 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 que 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.
L’opérateur raccourci ?
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 ? :
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> {
let mut fichier_pseudo = File::open("hello.txt")?;
let mut pseudo = String::new();
fichier_pseudo.read_to_string(&mut pseudo)?;
Ok(pseudo)
}
}
? operatorLe ? 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 où il y a un impl From<AutreErreur> for ErreurRetournee pour expliquer la conversion dans la fonction from du trait, l’opérateur ? se charge d’appeler la fonction from automatiquement.
Par exemple, nous pourrions changer la fonction lire_pseudo_depuis_fichier de l’encart 9-7 pour renvoyer un type d’erreur personnalisé nommé NotreErreur que nous définissons. Si nous définissons aussi impl From<io::Error> for NotreErreur pour construire une instance de NotreErreur à partir d’un io::Error, alors l’appel à l’opérateur ? dans le corps de lire_pseudo_depuis_fichier appellera from et convertira les types d’erreur sans qu’il soit nécessaire d’ajouter plus de code à cette fonction.
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 fichier_pseudo. 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 :
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn lire_pseudo_depuis_fichier() -> Result<String, io::Error> {
let mut pseudo = String::new();
File::open("hello.txt")?.read_to_string(&mut pseudo)?;
Ok(pseudo)
}
}
? operatorNous avons déplacé la création de la nouvelle String dans pseudo au début de la fonction ; cette partie n’a pas changé. Au lieu de créer la variable fichier_pseudo, 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 pseudo 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.
#![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")
}
}
fs::read_to_string instead of opening and then reading the fileRé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-il être utilisé
L’opérateur ? peut uniquement être utilisé dans des fonctions dont le type de retour est compatible avec ce sur quoi le ? est utilisé. C’est parce que l’opérateur ? est conçu pour retourner prématurément 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 fichier_salut = File::open("hello.txt")?;
}
? in the main function that returns () won’t compile.Ce code ouvre un fichier, ce qui devrait échouer. L’opérateur ? est placé 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:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let fichier_salut = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let fichier_salut = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 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ément à 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("
salut"), None);
}
? operator on an Option<T> valueCette 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 prend 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, s’il y a un caractère à la fin de la première ligne, il sera retourné dans la variante Some. L’opérateur ? au milieu 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ées 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 fichier_salut = File::open("hello.txt")?;
Ok(())
}
main to return Result<(), E> allows the use of the ? operator on Result values.Le type Box<dyn Error> est un objet trait, que nous verrons dans une section du chapitre 18. 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 valeur de type Err d’être retournée prématurément. Bien que le corps de la fonction main ne renvoie jamais que des erreurs de type std::io::Error, en spécifiant Box<dyn Error>, cette signature restera correcte même si du code renvoyant d’autres erreurs venait à être ajouté au corps de main.
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, lequel contient une fonction report qui renvoie un ExitCode. Pour plus d’information concernant l’implémentation du trait Termination, reportez-vous à la documentation de la bibliothèque standard
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.