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");
}

Encart 9-3 : ouverture d'un fichier

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),
    };
}

Encart 9-4 : utilisation de l'expression match pour gérer les variantes de Result qui peuvent être retournées

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)
            }
        },
    };
}

Encart 9-5 : gestion des différents cas d'erreurs avec des actions différentes

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 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 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é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 :

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),
    }
}
}

Encart 9-6 : une fonction qui retourne les erreurs au code qui l'appelle en utilisant match

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)
}
}

Encart 9-7 : une fonction qui retourne les erreurs au code appelant en utilisant l'opérateur ?

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)
}
}

Encart 9-8 : enchaînement des appels aux méthodes après l'opérateur ?

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")
}
}

Encart 9-9 : utilisation de fs::read_to_string plutôt que d'ouvrir puis lire le fichier

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")?;
}

Encart 9-10 : tentative d'utilisation du ? dans la fonction main qui retourne un (), qui ne devrait pas pouvoir se compiler

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);
}

Encart 9-11 : utilisation de l'opérateur ? sur une valeur du type Option<T>

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 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'aurions 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(())
}

Encart 9-12 : changement du main pour qu'elle retourne un Result<(), E> permettant d'utiliser l'opérateur ? sur des valeurs de type Result

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.