Aller au contenu

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.

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
}

.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 })
▶ Essayez par vous-même

ParseIntError { kind: InvalidDigit } est la façon dont Rust affiche l’erreur : le texte contient un caractère qui n’est pas un chiffre.

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("Nombre lu : {number}"),
Err(e) => println!("Conversion impossible : {e}"),
}
}
▶ Essayez par vous-même

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 : 5
Erreur : division par zéro impossible
▶ Essayez par vous-même

Renvoyer 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.

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 })
▶ Essayez par vous-même

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.

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.

É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 : 34
Erreur : 'abc' n'est pas un nombre
Erreur : 200 ans, ce n'est pas réaliste
▶ Essayez par vous-même

É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 })
▶ Essayez par vous-même
  • Result<T, E> représente une opération qui peut réussir (Ok) ou échouer (Err) avec une valeur d’erreur.
  • match traite explicitement les deux cas, sur le même modèle qu’Option.
  • L’opérateur ? renvoie automatiquement un Err plus tôt : pas besoin d’un match après chaque appel qui peut échouer.
  • Préférez Result à panic! pour les erreurs dont un appelant pourrait raisonnablement se remettre.

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