
Introduction à la gestion des erreurs avec `Option` et `Result`
Découvrez comment Rust gère l'absence de valeur et les erreurs récupérables avec les types `Option<T>` et `Result<T, E>`, et comprenez l'utilisation (et les dangers) de `unwrap()` et `expect()`.
La robustesse par le type : l'approche de Rust pour les erreurs
Contrairement à de nombreux langages qui utilisent des exceptions ou des valeurs spéciales comme `null` ou `undefined` pour signaler des erreurs ou l'absence de valeur, Rust adopte une approche plus explicite et sécurisée grâce à son système de types. Au lieu de laisser ces cas potentiellement problématiques non gérés ou cachés, Rust nous oblige à les considérer au moment de la compilation. Les deux outils principaux pour cela sont les types énumérés (enums) `Option<T>` et `Result<T, E>`, fournis par la bibliothèque standard.
Cette approche présente plusieurs avantages :
- Explicitness : Le type d'une fonction indique clairement si elle peut échouer ou retourner une valeur optionnelle. Il n'y a pas de surprise cachée.
- Sécurité à la compilation : Le compilateur Rust vous force à gérer les cas `None` (pour `Option`) et `Err` (pour `Result`), ce qui prévient de nombreuses erreurs d'exécution courantes comme les déréférencements de pointeurs nuls.
- Clarté : Le code devient plus facile à raisonner car les chemins d'erreur sont clairement définis dans le système de types.
Ce chapitre vous introduira à ces deux types fondamentaux, vous montrera comment les utiliser pour représenter l'absence de valeur ou les issues d'opérations faillibles, et abordera les méthodes de base pour interagir avec eux, y compris l'utilisation (et les mises en garde) de `unwrap()` et `expect()`.
Le type `Option<T>` : gérer l'absence de valeur
Le type `Option<T>` est utilisé lorsqu'une valeur pourrait être présente ou absente. Il est particulièrement utile pour remplacer le concept de `null` que l'on trouve dans d'autres langages, mais d'une manière beaucoup plus sûre. `Option<T>` est une énumération avec deux variantes :
- `Some(T)`: Indique que la valeur est présente et encapsule cette valeur de type `T`.
- `None`: Indique que la valeur est absente.
Par exemple, une fonction qui recherche un élément dans une collection pourrait retourner `Option
fn trouver_premier_pair(nombres: &[i32]) -> Option {
for &nombre in nombres {
if nombre % 2 == 0 {
return Some(nombre); // Valeur trouvée et encapsulée dans Some
}
}
None // Aucune valeur paire trouvée
}
fn main() {
let liste1 = [1, 3, 5, 6, 7];
let liste2 = [1, 3, 5, 9];
match trouver_premier_pair(&liste1) {
Some(valeur) => println!("Premier pair dans liste1 : {}", valeur),
None => println!("Aucun pair dans liste1."),
}
match trouver_premier_pair(&liste2) {
Some(valeur) => println!("Premier pair dans liste2 : {}", valeur), // Ne sera pas atteint
None => println!("Aucun pair dans liste2."),
}
} L'utilisation du `match` est la manière la plus idiomatique et la plus sûre de gérer un `Option<T>`, car elle vous oblige à considérer explicitement les deux cas (`Some` et `None`). Tenter d'utiliser directement la valeur contenue dans un `Option` sans vérifier sa présence conduirait à une erreur de compilation, ce qui prévient les fameuses "null pointer exceptions".
Le type `Result<T, E>` : gérer les opérations pouvant échouer
Le type `Result<T, E>` est utilisé pour les opérations qui peuvent soit réussir et retourner une valeur de type `T`, soit échouer et retourner une erreur de type `E`. C'est le mécanisme principal de Rust pour la gestion des erreurs récupérables. Comme `Option<T>`, `Result<T, E>` est une énumération, avec deux variantes :
- `Ok(T)`: Indique que l'opération a réussi et encapsule la valeur de succès de type `T`.
- `Err(E)`: Indique que l'opération a échoué et encapsule la valeur d'erreur de type `E`.
Par exemple, une fonction qui tente d'ouvrir un fichier pourrait retourner `Result
use std::fs::File;
use std::io;
fn ouvrir_fichier(nom_fichier: &str) -> Result {
let f = File::open(nom_fichier);
// File::open retourne déjà un Result
// Nous pouvons le retourner directement, ou le traiter.
match f {
Ok(fichier) => Ok(fichier),
Err(erreur) => {
// On pourrait vouloir logguer l'erreur ou la transformer ici
println!("Erreur à l'ouverture du fichier '{}': {}", nom_fichier, erreur);
Err(erreur)
}
}
}
fn main() {
match ouvrir_fichier("existe.txt") { // Supposons que existe.txt existe et est lisible
Ok(f) => println!("Fichier 'existe.txt' ouvert avec succès : {:?}", f),
Err(e) => println!("Erreur avec existe.txt : {}", e),
}
match ouvrir_fichier("n_existe_pas.txt") { // Supposons que ce fichier n'existe pas
Ok(f) => println!("Fichier 'n_existe_pas.txt' ouvert : {:?}", f), // Ne sera pas atteint
Err(e) => println!("Erreur avec n_existe_pas.txt : {}", e),
}
} De même qu'avec `Option`, `match` est la méthode privilégiée pour gérer un `Result`, vous forçant à traiter explicitement le cas de succès (`Ok`) et le cas d'erreur (`Err`). Cela rend la gestion des erreurs robuste et intégrée au flux de contrôle du programme.
Utilisation basique de `unwrap()` et `expect()` (et leurs dangers)
Bien que `match` soit la méthode la plus sûre, `Option<T>` et `Result<T, E>` fournissent des méthodes de raccourci pour accéder à la valeur interne, comme `unwrap()` et `expect()`. Cependant, ces méthodes doivent être utilisées avec une extrême prudence.
`unwrap()` :
Pour `Option<T>` : si la valeur est `Some(valeur)`, `unwrap()` retourne `valeur`. Si c'est `None`, `unwrap()` provoque un `panic!` (arrêt brutal du programme).
Pour `Result<T, E>` : si la valeur est `Ok(valeur)`, `unwrap()` retourne `valeur`. Si c'est `Err(erreur)`, `unwrap()` provoque un `panic!`.
// Exemple avec Option
let cinq = Some(5);
println!("Valeur : {}", cinq.unwrap()); // Affiche 5
// let aucun: Option = None;
// println!("{}", aucun.unwrap()); // PANIC!
// Exemple avec Result
let succes: Result = Ok(10);
println!("Succès : {}", succes.unwrap()); // Affiche 10
// let echec: Result = Err("grosse erreur");
// println!("{}", echec.unwrap()); // PANIC! `expect(message: &str)` :
Similaire à `unwrap()`, mais si un `panic!` se produit, le message que vous fournissez à `expect()` sera affiché. C'est légèrement mieux que `unwrap()` car cela donne plus de contexte sur l'origine du `panic!`.
// Exemple avec Option
let peut_etre_nom: Option<&str> = None;
// let nom = peut_etre_nom.expect("Le nom aurait dû être présent !"); // PANIC avec ce message
// Exemple avec Result
use std::net::IpAddr;
let adresse_ip_texte = "127.0.0.1";
let adresse: IpAddr = adresse_ip_texte.parse().expect("Adresse IP invalide");
println!("Adresse IP valide : {}", adresse);
// let adresse_ip_invalide_texte = "pas.une.ip";
// let adresse_invalide: IpAddr = adresse_ip_invalide_texte.parse().expect("Adresse IP invalide"); // PANIC!Les dangers : L'utilisation de `unwrap()` ou `expect()` transforme une erreur potentiellement récupérable (dans le cas de `Result`) ou une absence de valeur gérable (dans le cas d'`Option`) en un `panic!`, qui est une erreur non récupérable et qui arrête votre programme. En général, leur usage est déconseillé dans du code de production robuste, sauf si vous avez une certitude absolue (par exemple, une invariant logique prouvé) que la valeur sera toujours `Some` ou `Ok`.
Ils peuvent être acceptables dans des exemples, des tests, ou pour des prototypes rapides où un `panic!` est préférable à une gestion d'erreur complexe temporairement. Cependant, pour du code destiné à être fiable, privilégiez toujours `match`, ou d'autres combinateurs comme `map`, `and_then`, l'opérateur `?` (pour la propagation d'erreurs dans `Result`), ou des méthodes comme `unwrap_or`, `unwrap_or_else` qui fournissent des valeurs par défaut ou exécutent du code en cas de `None` ou `Err` sans paniquer.