Déstructuration et patterns
En Rust, la déstructuration n’est pas réservée au match : elle est disponible partout où le langage attend un motif (pattern), y compris dans les paramètres de fonction. C’est ce mécanisme qui explique des syntaxes déroutantes au premier abord, comme les extracteurs du framework web Axum.
Un motif, pas seulement un nom de variable
N’importe quel paramètre de fonction peut utiliser un motif, et pas uniquement un identifiant. La partie à gauche du : est un motif, celle de droite un type.
Dans f1, le motif se réduit au nom de la variable. Dans f2, le motif Etat(etat) signifie deux choses :
- « j’attends une valeur de type
Etat<...>» ; - « déconstruis-la, prends le champ interne et nomme-le
etat».
C’est exactement le mécanisme du match :
ou celui du let :
Les structures tuples
Le type utilisé ci-dessus est une structure tuple : une structure dont les champs n’ont pas de nom mais sont accessibles par index.
Le nom Etat sert alors à la fois de constructeur (Etat(42)) et de motif (Etat(valeur)). C’est la même dualité que pour les variantes d’énumération, comme Some(x).
Déstructurer les structures nommées
Le motif .. permet d’ignorer les champs restants, ce qui évite d’avoir à modifier tous les appels quand la structure s’enrichit :
L’imbrication fonctionne également :
Déstructurer les énumérations
Les autres emplacements
La déstructuration est possible dans tous ces contextes :
Motifs avancés
Les alternatives s’imbriquent
Le | n’est pas réservé au premier niveau d’un bras : il s’utilise à l’intérieur d’un motif, ce qui évite de dupliquer l’enveloppe.
La liaison avec @
L’opérateur @ permet de tester et capturer simultanément :
Il s’applique aussi à un motif composé, pour conserver la valeur entière tout en la déstructurant :
Le reste : ..
Le motif .. s’emploie dans les trois familles de conteneurs, avec à chaque fois le même sens — « ignorer ce qui n’est pas nommé » :
Les motifs de slice
Combiné à @, le reste peut être capturé au lieu d’être ignoré. C’est le moyen idiomatique de traiter une séquence tête/queue :
Warning
Un identifiant seul dans un motif lie toujours une nouvelle variable ; il ne compare pas à une variable existante. C’est une source d’erreur classique :
L’exception, c’est la constante : un chemin vers une const ou une variante d’énumération est bien compris comme une comparaison.
C’est la raison pour laquelle la convention de nommage en majuscules des constantes n’est pas seulement cosmétique : elle rend visible, à la lecture d’un motif, la différence entre comparer et lier.
Ignorer une valeur : _ et _nom
Les deux formes font taire l’avertissement de variable inutilisée, mais elles ne se comportent pas du tout pareil vis-à-vis de la propriété.
_ est un motif à part entière : il ne lie rien, donc ne déplace pas la valeur.
_nom est un identifiant ordinaire, qui commence simplement par un tiret bas : il lie, donc il déplace.
Tip
Le cas le plus fréquent est celui des gardes (guards) rendues par une bibliothèque : let _ = verrou.lock(); libère le verrou immédiatement, puisque la valeur temporaire n’est liée à rien. let _garde = verrou.lock(); le conserve jusqu’à la fin de la portée. Le comportement souhaité est presque toujours le second.
Modes de liaison
La question sous-jacente à tout motif est : la variable liée reçoit-elle la valeur, ou une référence vers elle ?
ref et ref mut
Historiquement, la réponse se donnait explicitement dans le motif : ref produit une référence partagée, ref mut une référence mutable, là où un identifiant nu aurait déplacé la valeur.
ref est un mot-clé du motif, à ne pas confondre avec &, qui appartient à l’expression : let ref a = x; équivaut à let a = &x;.
Les modes de liaison par défaut
Depuis Rust 2018, les match ergonomics rendent ref largement inutile. Lorsque l’on filtre une référence vers un conteneur, le compilateur bascule automatiquement le mode de liaison en « par référence », et propage ce mode aux motifs imbriqués.
C’est ce qui explique qu’un match sur &self.champ ou une boucle for (cle, valeur) in &table fournissent des références sans annotation particulière.
Le motif &
À l’inverse, un & dans le motif déréférence explicitement : il consomme la référence et lie la valeur pointée.
Le second n’est possible que parce que i32 implémente Copy. Sur un type possédé, il tenterait un déplacement hors d’une référence et échouerait avec E0507. C’est également la forme que l’on rencontre dans les closures, où |&x| évite un déréférencement manuel :
mut dans un motif
Enfin, mut rend la variable liée modifiable, ce qui est indépendant de la mutabilité de la source :
Note
Quatre écritures voisines, quatre rôles distincts :
| Écriture | Effet |
|---|---|
Some(s) | Lie par valeur, ou par référence si le mode par défaut l’impose |
Some(ref s) | Force la liaison par référence — rarement nécessaire aujourd’hui |
Some(mut s) | Lie par valeur, en rendant la variable modifiable |
&Some(s) | Déréférence la référence filtrée, puis lie la valeur |
Exhaustivité
Un match doit couvrir tous les cas possibles ; le compilateur refuse le code sinon.
C’est une propriété précieuse à l’usage : ajouter une variante à une énumération fait échouer la compilation de tous les match qui l’ignorent, ce qui transforme une omission silencieuse en liste de travail fournie par le compilateur.
Warning
Ajouter un bras _ => {} par confort annule ce bénéfice : les variantes futures y tomberont silencieusement. Il est préférable d’énumérer les variantes, et de réserver _ aux cas où le domaine est réellement ouvert, comme les entiers.
L’attribut #[non_exhaustive] permet à une bibliothèque de se réserver le droit d’ajouter des variantes sans casser ses utilisateurs. En dehors de la crate qui le définit, un type ainsi marqué impose un bras attrape-tout :
Motifs réfutables et irréfutables
Rust distingue deux catégories de motifs :
| Catégorie | Peut échouer | Contextes autorisés |
|---|---|---|
| Irréfutable | Non — il correspond toujours | let, paramètres de fonction, for |
| Réfutable | Oui | match, if let, while let, let ... else |
C’est la raison pour laquelle un paramètre de fonction ne peut jamais déstructurer une énumération à plusieurs variantes : le compilateur doit avoir la garantie que le motif correspond systématiquement. Une structure tuple à variante unique, elle, satisfait cette contrainte — d’où la possibilité d’écrire Etat(etat): Etat<Config> en paramètre.
Cas concret : les extracteurs Axum
Un extracteur est un type qui sait prélever une information dans une requête HTTP entrante : un segment d’URL, le corps JSON, un en-tête, l’état partagé. Le framework Axum est bâti sur ce principe — les paramètres du gestionnaire sont déclarés avec des types extracteurs, qu’Axum remplit à partir de la requête avant d’appeler la fonction.
Tout type implémentant FromRequest ou FromRequestParts peut servir d’extracteur.
State<T> n’est rien d’autre qu’une structure tuple, dont voici la forme simplifiée :
La syntaxe State(etat): State<AppState> se décompose donc ainsi :
Pourquoi le nom apparaît deux fois
La répétition de State à gauche et à droite est inévitable, car les deux moitiés portent des informations indépendantes :
- le type indique à Axum quel extracteur employer ;
- le motif indique comment nommer la valeur extraite.
Rust ne peut pas déduire le type à partir du motif dans une signature de fonction.
Sans déstructuration
Écrire le paramètre sans motif reste parfaitement légal :
La seconde forme repose sur Le mécanisme Deref. Les deux sont correctes, mais la déstructuration élimine l’enveloppe dès la signature et donne directement un nom parlant à la valeur utile.
À retenir
Partout où Rust attend un nom de variable, il accepte en réalité un motif.
Cette généralisation traverse tout le langage : let, for, les closures, les paramètres de fonction, match, if let, while let, let ... else. La syntaxe des extracteurs Axum n’est pas une astuce du framework, mais une simple application de cette règle à une structure tuple.
Ressources
- Patterns and Matching — chapitre du Rust Book
- Pattern syntax — la référence exhaustive de la syntaxe des motifs
- Axum — extractors — la liste des extracteurs fournis par le framework