Result en Rust
Là où Option représente une valeur qui peut être absente, Result représente une opération qui peut échouer. Et contrairement à une valeur absente, un échec s’accompagne généralement d’une raison.
À quoi ressemble Result
Section intitulée « À quoi ressemble Result »Result est une autre enum de la bibliothèque standard, cette fois avec deux variantes qui contiennent chacune des données :
enum Result<T, E> { Ok(T), // succès, avec une valeur Err(E), // échec, avec une erreur}Une fonction qui peut échouer
Section intitulée « Une fonction qui peut échouer ».parse(), que vous avez vue sur la page Conversion de types, en est un bon exemple concret : transformer du texte en nombre peut échouer si le texte n’est pas vraiment un nombre :
fn main() { let good: Result<i32, std::num::ParseIntError> = "42".parse(); let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good); println!("{:?}", bad);}Sortie :
Ok(42)Err(ParseIntError { kind: InvalidDigit })ParseIntError { kind: InvalidDigit } est la façon dont Rust affiche l’erreur : le texte contient un caractère qui n’est pas un chiffre.
Traiter un Result avec match
Section intitulée « Traiter un Result avec match »fn main() { let input = "42";
match input.parse::<i32>() { Ok(number) => println!("Nombre lu : {number}"), Err(e) => println!("Conversion impossible : {e}"), }}C’est la même forme que pour traiter une Option avec match, sauf que la branche Err vous donne une vraie valeur d’erreur à examiner ou à signaler, et pas seulement « il n’y avait rien ».
Écrire votre propre fonction qui renvoie un Result
Section intitulée « Écrire votre propre fonction qui renvoie un Result »fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("division par zéro impossible")) } else { Ok(a / b) }}
fn main() { match divide(10.0, 2.0) { Ok(result) => println!("Résultat : {result}"), Err(e) => println!("Erreur : {e}"), }
match divide(10.0, 0.0) { Ok(result) => println!("Résultat : {result}"), Err(e) => println!("Erreur : {e}"), }}Sortie :
Résultat : 5Erreur : division par zéro impossibleRenvoyer Result<f64, String> oblige chaque appelant à tenir compte d’un éventuel échec : impossible de traiter par mégarde une division par zéro comme un nombre normal.
L’opérateur ?
Section intitulée « L’opérateur ? »Enchaîner plusieurs opérations qui peuvent chacune échouer mène à des blocs match imbriqués les uns dans les autres. L’opérateur ? aplatit tout cela. Il signifie : « si c’est un Err, renvoie immédiatement cette erreur depuis la fonction en cours ; sinon, donne-moi la valeur contenue dans le Ok ».
fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> { let number = input.parse::<i32>()?; // sort tout de suite si la conversion échoue Ok(number * 2)}
fn main() { println!("{:?}", parse_and_double("21")); println!("{:?}", parse_and_double("oups"));}Sortie :
Ok(42)Err(ParseIntError { kind: InvalidDigit })Sans ?, cette ligne aurait besoin de son propre match, juste pour extraire number ou sortir tout de suite avec l’erreur. ? fait exactement cela, en un seul caractère. Il ne fonctionne que dans une fonction qui renvoie elle-même un Result (ou une Option), car c’est ce qui lui donne un endroit où renvoyer l’erreur.
Result ou panic! ?
Section intitulée « Result ou panic! ? »Rust dispose aussi de panic!, qui arrête immédiatement le programme : nous le verrons plus en détail sur la page suivante. En règle générale, utilisez Result pour les échecs qu’un appelant peut raisonnablement vouloir gérer (une saisie invalide, un fichier manquant) ; réservez panic! aux situations qui révèlent un vrai bug, quand continuer serait pire que s’arrêter.
Dès que vos fonctions peuvent échouer de plusieurs façons différentes, une simple String comme erreur ne suffit plus. La page Erreurs personnalisées montre comment définir votre propre type d’erreur, et la page anyhow et thiserror présente les crates qu’utilisent la plupart des projets Rust pour réduire le code répétitif.
Exercices
Section intitulée « Exercices »1. Vérifier un âge
Section intitulée « 1. Vérifier un âge »Écrivez fn parse_age(input: &str) -> Result<u32, String>. Elle doit renvoyer un Err avec un message utile si le texte n’est pas un nombre, ou si le nombre dépasse 150. Testez-la avec "34", "abc" et "200". Indice : faites un match sur input.parse::<u32>(), et dans la branche Ok, utilisez un if pour vérifier la valeur.
Voir la solution
fn parse_age(input: &str) -> Result<u32, String> { match input.parse::<u32>() { Ok(age) => { if age > 150 { Err(format!("{age} ans, ce n'est pas réaliste")) } else { Ok(age) } } Err(_) => Err(format!("'{input}' n'est pas un nombre")), }}
fn main() { for input in ["34", "abc", "200"] { match parse_age(input) { Ok(age) => println!("Âge : {age}"), Err(e) => println!("Erreur : {e}"), } }}Sortie :
Âge : 34Erreur : 'abc' n'est pas un nombreErreur : 200 ans, ce n'est pas réaliste2. Additionner deux nombres avec ?
Section intitulée « 2. Additionner deux nombres avec ? »Écrivez fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>, qui convertit les deux chaînes en nombres et renvoie leur somme. Utilisez ? au lieu de match. Affichez le résultat pour ("10", "20") et pour ("10", "vingt"). Indice : il faut un ? par parse, et la dernière ligne enveloppe la somme dans Ok(...).
Voir la solution
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> { let x = a.parse::<i32>()?; let y = b.parse::<i32>()?; Ok(x + y)}
fn main() { println!("{:?}", add_strings("10", "20")); println!("{:?}", add_strings("10", "vingt"));}Sortie :
Ok(30)Err(ParseIntError { kind: InvalidDigit })Récapitulatif
Section intitulée « Récapitulatif »Result<T, E>représente une opération qui peut réussir (Ok) ou échouer (Err) avec une valeur d’erreur.matchtraite explicitement les deux cas, sur le même modèle qu’Option.- L’opérateur
?renvoie automatiquement unErrplus tôt : pas besoin d’unmatchaprès chaque appel qui peut échouer. - Préférez
Resultàpanic!pour les erreurs dont un appelant pourrait raisonnablement se remettre.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quelles sont les deux variantes de Result<T, E> ?
2. Que fait l'opérateur ? dans une fonction qui renvoie un Result ?
3. D'après cette page, quand faut-il préférer Result à panic! ?
4. Dans l'exemple divide de cette page, que renvoie divide(10.0, 0.0) ?
5. Pourquoi l'opérateur ? ne fonctionne-t-il que dans une fonction qui renvoie elle-même un Result (ou une Option) ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Recoverable Errors with Result — The Rust Book (en anglais) : le chapitre officiel sur
Result,unwrap,expectet l’opérateur?, avec des exemples d’ouverture de fichiers. std::result— documentation de la bibliothèque standard (en anglais) : une vue d’ensemble du travail avecResult, y compris des méthodes commemap_err,unwrap_oretok.- Result — Rust by Example (en anglais) : des exemples exécutables qui traitent un
Resultet le renvoient depuismain. - The ? operator — Rust by Example (en anglais) : un court passage centré sur la façon dont
?remplace les blocsmatchimbriqués.