Acheter version papier

Rust

Fork me on GitHub

I. Les bases de la programmation en Rust

12. Gestion des erreurs

Il est courant dans d'autres langages de voir ce genre de code :

Objet *obj = creer_objet();

if (obj == NULL) {
    // gestion de l'erreur
}

Vous ne verrez (normalement) pas ça en Rust.

Result

Créons un fichier par exemple :

Runuse std::fs::File;

let mut fichier = File::open("fichier.txt");

La documentation dit que File::open renvoie un Result. Il ne nous est donc pas possible d'utiliser directement la variable fichier. Cela nous "oblige" à vérifier le retour de File::open :

Runuse std::fs::File;

let mut fichier = match File::open("fichier.txt") {
    Ok(f) => {
        // L'ouverture du fichier s'est bien déroulée, on renvoie l'objet
        f
    }
    Err(e) => {
        // Il y a eu un problème, affichons l'erreur pour voir ce qu'il se passe
        println!("erreur : {:?}", e);
        // On ne peut pas renvoyer le fichier ici, donc on quitte la fonction
        return;
    }
};

Il est cependant possible de passer outre cette vérification, mais c'est à vos risques et périls !

Runuse std::fs::File;

let mut fichier = File::open("fichier.txt").expect("erreur lors de l'ouverture");

Si jamais il y a une erreur lors de l'ouverture du fichier, votre programme plantera et vous ne pourrez rien y faire. Il est toutefois possible d'utiliser cette méthode de manière "sûre" avec les fonctions is_ok et is_err :

Runuse std::fs::File;

let mut fichier = File::open("fichier.txt");

if fichier.is_ok() {
    // On peut faire expect !
} else {
    // Il y a eu une erreur, expect impossible !
}

Utiliser le pattern matching est cependant préférable.

À noter qu'il existe un équivalent de la méthode expect qui s'appelle unwrap. Elle fait exactement la même chose mais ne permet pas de fournir un message d'erreur. Pour faire simple : toujours préférer expect à unwrap !

Option

Vous savez maintenant qu'il n'est normalement pas possible d'avoir des objets invalides. Exemple :

Runlet mut v = vec![1, 2];

v.pop(); // retourne Some(2)
v.pop(); // retourne Some(1)
v.pop(); // retourne None

Cependant, il est tout à fait possible que vous ayez besoin d'avoir un objet qui serait initialisé plus tard pendant le programme ou qui vous permettrait de vérifier un état. Dans ce cas comment faire ? Option est là pour ça !

Imaginons que vous ayez un vaisseau customisable sur lequel il est possible d'avoir des bonus (disons un salon intérieur). Il ne sera pas là au départ, mais peut être ajouté par la suite :

Runstruct Vaisseau {
    // Pleins de champs
    salon: Option<Salon>,
}

impl Vaisseau {
    pub fn new() -> Vaisseau {
        Vaisseau {
            // On initialise les autres champs
            salon: None, // On n'a pas de salon
        }
    }
}

let mut vaisseau = Vaisseau::new();

Si jamais vous voulez tester le code, vous pouvez utiliser ce code pour la structure Salon:

Run// On définit une structure "Salon" vide pour l'exemple.
struct Salon {}

impl Salon {
    fn new() -> Salon {
        Salon {}
    }
}

Donc pour le moment, on n'a pas de salon. Maintenant nous en ajoutons un :

Runvaisseau.salon = Some(Salon::new());

Je présume que vous vous demandez comment accéder au salon maintenant. Tout simplement comme ceci :

Runmatch vaisseau.salon {
    Some(s) => {
        println!("ce vaisseau a un salon");
    }
    None => {
        println!("ce vaisseau n'a pas de salon");
    }
}

Au début, vous risquez de trouver ça agaçant, mais la sécurité que cela apporte est un atout non négligeable ! Cependant, tout comme avec Result, vous pouvez utiliser la méthode expect.

Runvaisseau.salon = Some(Salon::new());

// Pas recommandé !!!
let salon =  vaisseau.salon.expect("pas de salon");

Tout comme avec Result, il est possible de se passer du mécanisme de pattern matching avec les méthodes is_some et is_none :

Runif vaisseau.salon.is_some() {
    // On peut utiliser expect !
} else {
    // Ce vaisseau ne contient pas de salon !
}

Encore une fois, utiliser le pattern matching est préférable.

panic!

panic! est une macro très utile puisqu'elle permet de "quitter" le programme. Elle n'est à appeler que lorsque le programme subit une erreur irrécupérable. Elle est très simple d'utilisation :

Runpanic!();
// panic avec une valeur de 4 pour la récupérer ailleurs (hors
// du programme par exemple)
panic!(4);
panic!("Une erreur critique vient d'arriver !");
panic!("Une erreur critique vient d'arriver : {}", "le moteur droit est mort");

Et c'est tout.

Question !

Pour les codes que nous avons vu au-dessus, il serait actuellement possible de les écrire de manière plus courte :

Runuse std::fs::File;
use std::io;

fn foo() -> io::Result<u32> {
    let mut fichier = File::open("fichier.txt")?;
    // ...
    Ok(0)
}

La différence étant que nous avons utilisé l'opérateur ?. Pour pouvoir s'en servir, plusieurs conditions doivent être réunies. Tout d'abord, on ne peut utiliser ? que sur des types implémentant le trait Try (nous reviendrons sur ce qu'est un trait dans un prochain chapitre). Il faut aussi que la fonction renvoie la même chose que le type sur lequel on utilise le ? (c'est pourquoi notre fonction foo renvoie io::Result). Dans le cas où votre fonction ne renvoie pas la même chose, il est possible de changer l'erreur pour que ça corresponde :

Runuse std::fs::File;

fn foo() -> Result<u32, String> {
    let mut fichier = File::open("fichier.txt")
        // On change io::Error en String avec "map_err" si File::open
        // renvoie une erreur.
        .map_err(|e| format!("open error {:?}", e))?;
    Ok(0)
}

Voilà pour ce chapitre, vous devriez maintenant être capables de créer des codes un minimum "sécurisés".