Le type Option

Rust ne possède ni null ni undefined. L’absence de valeur est représentée par un type à part entière, Option<T>, que le compilateur oblige à traiter. Cette page recense les usages idiomatiques du type.

Choisir le bon outil

SituationPatternExemple
Transformer la valeur si elle est présentemapopt.map(|s| s.len())
Transformer en produisant un autre Optionand_thenopt.and_then(|s| s.parse().ok())
Ne garder que si un prédicat est vérifiéfilteropt.filter(|s| !s.is_empty())
Fournir une valeur par défautunwrap_or_else, unwrap_or_defaultopt.unwrap_or_default()
Sortie anticipée puis code linéairelet ... elselet Some(x) = opt else { return; };
Propager l’absence à l’appelant?let x = opt?;
L’absence est une erreur pour l’appelantok_or_elseopt.ok_or_else(|| Erreur::Absent)?
Un seul cas intéresse, avec effet de bordif letif let Some(x) = opt { ... }
Les deux branches font un vrai travailmatchvoir plus bas
Test booléenis_some_and, is_none_oropt.is_some_and(|x| x > 3)
Tip

Règle empirique : si un match comporte une branche None => None ou None => valeur_constante, c’est un combinateur déguisé. Il gagne à être réécrit.

Modèle mental

Option<T> n’est pas un T « nullable » : c’est une énumération à deux variantes, comme on pourrait en écrire soi-même.

enum Option<T> {
    None,
    Some(T),
}

Trois conséquences pratiques :

  • Pas de propagation implicite. Là où d’autres langages proposent un chaînage optionnel (a?.b?.c), Rust impose d’écrire explicitement la chaîne, avec and_then ou l’opérateur ?.
  • Aucun surcoût dans les cas courants. Grâce à l’optimisation du pointeur nul, Option<Box<T>>, Option<&T> ou Option<NonZeroU32> occupent exactement la même place que le type nu : le discriminant est encodé dans une valeur autrement impossible.
  • Option est annoté #[must_use]. Ignorer un Option retourné déclenche un avertissement du compilateur.
use std::mem::size_of;

assert_eq!(size_of::<Option<&u8>>(), size_of::<&u8>());     // 8 octets, pas 16
assert_eq!(size_of::<Option<Box<u8>>>(), size_of::<Box<u8>>());
Info

Une analogie utile : Option<T> se comporte comme un Vec<T> de capacité 1. Ce n’est pas qu’une image — Option implémente IntoIterator, et nombre de ses combinateurs portent le nom de leur équivalent sur Iterator, avec la même sémantique : map, filter, flatten, zip.

Option ou Result ?

Option<T>Result<T, E>
SémantiqueL’absence est normale et attendueQuelque chose a échoué
L’appelant a besoin de savoir pourquoiNonOui
ExemplesHashMap::get, Iterator::next, champ facultatifEntrées/sorties, analyse syntaxique, validation

S’il faut un commentaire pour expliquer ce que None signifie à cet endroit, c’est probablement un Result accompagné d’une énumération d’erreurs qui convient.

Extraire la valeur

let ... else

Stabilisé en Rust 1.65, c’est le pattern par défaut pour une sortie anticipée : il déstructure ou diverge, sans indenter la suite du code.

fn traiter(config: &Config) -> Result<Rapport, Erreur> {
    let Some(racine) = &config.racine else {
        return Err(Erreur::RacineManquante);
    };
    let Some(profondeur) = config.profondeur else {
        return Err(Erreur::ProfondeurManquante);
    };

    // Ici racine et profondeur sont des valeurs, plus des Option
    construire_rapport(racine, profondeur)
}

Le bloc else doit diverger — return, break, continue, panic! — et le compilateur le vérifie. Comparée à des match imbriqués, la version let ... else économise deux niveaux d’indentation et garde la logique métier à gauche.

if let

Lorsqu’un seul cas est intéressant :

if let Some(valeur) = cache.get(&cle) {
    return valeur.clone();
}

Avec un else quand les deux branches sont courtes :

if let Some(nom) = &utilisateur.nom_affiche {
    println!("Bonjour {nom}");
} else {
    println!("Bonjour, inconnu");
}
Chaînage de let (let chains)

Depuis Rust 1.88 et l’édition 2024, plusieurs conditions peuvent être combinées :

if let Some(noeud) = arbre.get(id)
    && noeud.est_repertoire()
    && let Some(parent) = noeud.parent
{
    // ...
}

Avant cette version, ou hors édition 2024, il faut imbriquer les blocs ou passer par is_some_and.

while let

Pour consommer jusqu’à épuisement :

while let Some(element) = pile.pop() {
    traiter(element);
}

match

match reste justifié quand les deux branches effectuent un travail réel :

match self.ecriture_en_attente.take() {
    Some(tampon) => {
        self.vider(tampon)?;
        self.stats.vidages += 1;
    }
    None => {
        self.stats.vidages_inutiles += 1;
    }
}
Note

Sur un &Option<T>, les variables liées deviennent automatiquement des références, sans avoir à écrire ref :

match &self.nom {
    Some(nom) => nom.len(),     // nom est un &String
    None => 0,
}

Ce sont les modes de liaison par défaut, détaillés dans Déstructuration et patterns. Dans ce cas précis, self.nom.as_ref().map_or(0, |n| n.len()) reste toutefois plus idiomatique.

Transformer : les combinateurs

C’est le cas majoritaire dans du code réel.

Les fondamentaux

opt.map(f)              // Option<T> -> Option<U>, transforme la valeur
opt.and_then(f)         // f: T -> Option<U>, l'équivalent de flatMap
opt.filter(pred)        // Conserve Some si pred(&T) est vrai
opt.or(autre)           // Le premier Some des deux
opt.or_else(f)          // Idem, mais l'alternative est calculée paresseusement
opt.xor(autre)          // Some si et seulement si exactement un des deux l'est
opt.zip(autre)          // Option<A> + Option<B> -> Option<(A, B)>
opt.flatten()           // Option<Option<T>> -> Option<T>

Sortir de l’Option avec une valeur de repli

opt.unwrap_or(defaut)                   // defaut TOUJOURS évalué : réserver aux littéraux
opt.unwrap_or_else(|| couteux())        // Paresseux : dès qu'il y a un calcul
opt.unwrap_or_default()                 // Requiert T: Default
opt.map_or(0, |x| x.len())              // map + unwrap_or en une fois
opt.map_or_else(|| repli(), |x| x.len())
Warning

unwrap_or(String::new()) alloue systématiquement, y compris quand l’Option vaut Some. Il faut lui préférer unwrap_or_default() ou unwrap_or_else.

Tests booléens

opt.is_some()
opt.is_none()
opt.is_some_and(|x| x > 3)          // Depuis 1.70
opt.is_none_or(|x| x > 3)           // Depuis 1.82 : absent OU prédicat vérifié
matches!(opt, Some(x) if x > 3)     // Équivalent, plus verbeux

is_some_and remplace avantageusement opt.map(|x| x > 3).unwrap_or(false).

Effet de bord dans une chaîne

opt.inspect(|x| println!("trouvé : {x}"))       // Depuis 1.76

Une chaîne réaliste

Extraction d’un champ textuel facultatif, non vide et débarrassé de ses espaces :

let nom: Option<&str> = valeur
    .get("nom")
    .and_then(|v| v.as_str())       // Option<&Value> -> Option<&str>
    .map(str::trim)
    .filter(|s| !s.is_empty());

La version équivalente en match fait une douzaine de lignes et trois niveaux d’imbrication.

Tip

La lisibilité s’inverse au-delà de quatre maillons environ, ou dès que les closures contiennent une logique substantielle. Il faut alors couper en variables intermédiaires nommées, ou revenir à let ... else.

Références et propriété

C’est la principale source de friction. Un Option<T> possède son T : toute méthode qui prend self consomme l’Option.

La famille as_*

let opt: Option<String> = Some("Hello".into());

opt.as_ref()            // &Option<String>     -> Option<&String>
opt.as_mut()            // &mut Option<String> -> Option<&mut String>
opt.as_deref()          //                     -> Option<&str>      (via Deref)
opt.as_deref_mut()      //                     -> Option<&mut str>

as_ref() est le réflexe pour lire sans consommer :

// Ne compile pas : map() consomme self.nom, qui n'est qu'emprunté
fn longueur_nom(&self) -> usize {
    self.nom.map(|n| n.len()).unwrap_or(0)
}

// Correct
fn longueur_nom(&self) -> usize {
    self.nom.as_ref().map_or(0, |n| n.len())
}

as_deref() donne le type emprunté naturel, ce qui est déterminant pour les signatures publiques :

// Expose &str et non &String : l'appelant n'a pas à connaître la représentation interne
fn nom_affiche(&self) -> Option<&str> {
    self.nom.as_deref()
}

La même logique s’applique à Option<Vec<T>> → Option<&[T]> et Option<PathBuf> → Option<&Path>. Le mécanisme sous-jacent est décrit dans Le mécanisme Deref.

Revenir d’une référence à une valeur

let reference: Option<&String> = opt.as_ref();
let possedee: Option<String> = reference.cloned();      // Requiert T: Clone
let nombre: Option<i32> = ref_entier.copied();          // Requiert T: Copy, à préférer

Extraire une valeur depuis un &mut self

Il est impossible de déplacer une valeur hors d’une référence mutable. take() résout ce cas en laissant None à la place :

impl Ecrivain {
    fn vider(&mut self) -> std::io::Result<()> {
        if let Some(tampon) = self.en_attente.take() {
            self.sortie.write_all(&tampon)?;
        }
        Ok(())
    }
}

Méthodes voisines :

opt.replace(nouvelle)               // Place la nouvelle valeur et rend l'ancienne
opt.insert(valeur)                  // -> &mut T, écrase toujours
opt.get_or_insert(valeur)           // -> &mut T, insère seulement si None
opt.get_or_insert_with(|| f())
opt.take_if(|x| x.est_perime())     // Depuis 1.80 : take conditionnel
Info

Option::take est le pendant de std::mem::take pour les champs facultatifs. C’est le pattern canonique des automates à états et des constructeurs de type fn build(&mut self) -> Chose.

Propager

L’opérateur ?

Dans une fonction qui retourne elle-même un Option, ? court-circuite sur None :

fn nom_du_parent(arbre: &Arbre, id: NoeudId) -> Option<&str> {
    let noeud = arbre.get(id)?;
    let parent = arbre.get(noeud.parent?)?;
    parent.nom.as_deref()
}
Warning

? ne mélange pas Option et Result au sein d’une même fonction : il faut convertir explicitement de l’un vers l’autre.

Les ponts entre Option et Result

opt.ok_or(Erreur::Absent)                   // L'erreur est construite systématiquement
opt.ok_or_else(|| Erreur::Absent(id))       // Paresseux, à préférer
res.ok()                                    // Result<T, E> -> Option<T>, l'erreur est perdue
res.err()                                   // Result<T, E> -> Option<E>

Le cas typique dans un gestionnaire de requêtes :

fn recuperer(&self, id: NoeudId) -> Result<&Noeud, ApiErreur> {
    self.noeuds
        .get(&id)
        .ok_or_else(|| ApiErreur::Introuvable(format!("noeud {id}")))
}

transpose

transpose échange les deux couches :

let a: Option<Result<T, E>> = /* ... */;
let b: Result<Option<T>, E> = a.transpose();    // Et réciproquement

Le besoin apparaît typiquement lorsqu’un champ facultatif doit être analysé : absent, le résultat est valide sans valeur ; présent mais invalide, c’est une erreur.

let port: Option<u16> = brut
    .get("port")
    .map(|s| s.parse::<u16>())      // Option<Result<u16, _>>
    .transpose()?;                  // Result<Option<u16>, _>, puis ?

Interopérabilité avec les itérateurs

Option implémente IntoIterator et se comporte comme une séquence de zéro ou un élément, ce qui débloque plusieurs idiomes :

// Aplatir un itérateur d'Option en écartant les None
let noms: Vec<&str> = noeuds.iter().filter_map(|n| n.nom.as_deref()).collect();
let noms: Vec<&str> = noeuds.iter().map(|n| n.nom.as_deref()).flatten().collect();

// Tout ou rien : None dès qu'un seul élément est None
let tous: Option<Vec<u16>> = brut.iter().map(|s| s.parse().ok()).collect();

// Enchaîner une valeur facultative
let elements = base.iter().chain(supplement.iter());    // supplement: Option<&Element>

filter_map est la forme canonique, à préférer à .filter(Option::is_some).map(Option::unwrap).

Note

Côté Iterator, les méthodes next(), find(), max(), min() et last() retournent des Option : les combinateurs présentés ici s’enchaînent donc naturellement à leur suite.

Anti-patterns

is_some() suivi de unwrap()

// À éviter
if opt.is_some() {
    let v = opt.unwrap();
    traiter(v);
}

// Préférer
if let Some(v) = opt {
    traiter(v);
}

Le compilateur ne relie pas les deux instructions : unwrap reste une source potentielle de panique. Clippy le signale sous le nom unnecessary_unwrap.

Un match là où un combinateur suffit

// À éviter
let longueur = match &opt {
    Some(s) => s.len(),
    None => 0,
};

// Préférer
let longueur = opt.as_ref().map_or(0, |s| s.len());

unwrap() en production

unwrap() n’apprend rien à celui qui lit la trace d’appels. Trois solutions selon le cas :

opt.expect("racine validée au démarrage")   // Invariant documenté
opt.ok_or(Erreur::Absent)?                  // Remonte l'erreur
opt.unwrap_or_default()                     // Valeur de repli

Le message d’expect doit expliquer pourquoi la valeur ne peut pas être None, et non ce qui a échoué. unwrap() nu se justifie dans les tests, les prototypes et les scripts.

clone() pour contourner l’emprunteur

opt.clone().unwrap() sur un Option<String> dans une boucle chaude, c’est une allocation par tour. Dans la grande majorité des cas, as_ref() ou as_deref() résout le problème sans copie. Le clone() doit rester un choix délibéré.

Option<Vec<T>> là où le vide suffit

Si None et Some(vec![]) ont la même signification pour les appelants, ce sont deux représentations d’un même état. Un Vec<T> simple suffit. L’exception légitime : quand « non renseigné » et « renseigné vide » sont sémantiquement distincts, par exemple dans un formulaire ou une modification partielle.

Option<Option<T>> sans raison

C’est presque toujours un flatten() oublié. Le cas légitime est la sémantique d’un PATCH JSON, où None signifie « champ absent », Some(None) « mettre à null » et Some(Some(v)) « affecter ». L’intention mérite alors d’être documentée.

Un booléen accompagnant une valeur

// À éviter
struct Cache { possede_valeur: bool, valeur: String }

// Préférer
struct Cache { valeur: Option<String> }

Les états invalides deviennent impossibles à représenter. C’est l’argument principal en faveur d’Option.

Conception d’API

En paramètre, Option<&T> est préférable à &Option<T> : un appelant qui n’a pas d’Option sous la main peut passer Some(&x) sans en construire une.

fn rendre(&self, filtre: Option<&str>) -> String        // Souple
fn rendre(&self, filtre: &Option<String>) -> String     // Contraint l'appelant

En retour, exposer Option<&T> avec le type emprunté naturel (&str, &[T], &Path) au moyen d’as_deref().

Dans une structure, un Option par champ réellement facultatif. Si trois champs valent None ou Some ensemble, il vaut mieux extraire une sous-structure placée dans un unique Option.

Avec serde :

#[derive(Deserialize, Serialize)]
struct Config {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    racine: Option<PathBuf>,
}

Option combiné à #[serde(default)] rend le champ facultatif à la désérialisation, tandis que skip_serializing_if évite d’émettre des null.

Aide-mémoire

CONSTRUIRE
  Some(v) / None
  bool.then(|| v)          bool -> Option<T>  (paresseux)
  bool.then_some(v)        bool -> Option<T>

INSPECTER
  is_some / is_none / is_some_and / is_none_or
  matches!(opt, Some(x) if ...)

TRANSFORMER
  map / and_then / filter / flatten / zip / inspect

COMBINER
  or / or_else / xor / and

SORTIR
  unwrap_or / unwrap_or_else / unwrap_or_default
  map_or / map_or_else
  expect / unwrap                       (à justifier)

RÉFÉRENCES
  as_ref / as_mut / as_deref / as_deref_mut
  cloned / copied

MUTER EN PLACE
  take / take_if / replace / insert / get_or_insert / get_or_insert_with

VERS RESULT
  ok_or / ok_or_else / transpose        (et Result::ok / Result::err)

CONTRÔLE DE FLUX
  let ... else     sortie anticipée puis suite linéaire   <- par défaut
  if let           un seul cas
  while let        boucle
  ?                propagation
  match            deux branches non triviales

Ressources

  • std::option::Option — la documentation est courte et mérite une lecture intégrale
  • Clippy — cargo clippy détecte la majorité des anti-patterns ci-dessus : unnecessary_unwrap, option_if_let_else, manual_map, redundant_clone
  • Rust API Guidelines — section consacrée aux paramètres facultatifs