Structures, énumérations et méthodes
Rust n’a pas de classes. La modélisation des données repose sur deux constructions — les structures et les énumérations — auxquelles on attache des comportements via des blocs impl. L’héritage n’existe pas ; le partage de comportement passe par les traits.
Les structures
Trois formes existent, selon que les champs sont nommés, anonymes, ou absents.
La structure tuple sert principalement à créer un type distinct autour d’une valeur existante — ce que l’on appelle le patron newtype. Metres(f64) et Secondes(f64) ne sont alors pas interchangeables, alors que deux f64 le seraient.
La structure unité est utile pour implémenter un trait sur un type sans donnée, par exemple un marqueur ou une stratégie.
Instancier
Note
La mutabilité s’applique à la variable entière, jamais à un champ isolé. Il n’existe pas d’équivalent d’un champ final ou readonly : soit toute l’instance est modifiable, soit aucune partie ne l’est.
Deux raccourcis reviennent souvent. Le premier, l’initialisation abrégée, quand la variable porte déjà le nom du champ :
Le second, la syntaxe de mise à jour, pour reprendre les champs d’une autre instance :
Warning
La syntaxe de mise à jour déplace les champs non Copy. Ici, utilisateur.nom étant une String, utilisateur n’est plus utilisable dans son ensemble après l’opération — voir Ownership et emprunt. Le .. doit toujours figurer en dernier, et ne se termine pas par une virgule.
Les blocs impl
Le comportement est défini séparément des données, dans un ou plusieurs blocs impl.
L’appel se fait avec :: pour les fonctions associées, et . pour les méthodes :
Choisir la forme du receveur
| Receveur | Signification | Quand l’employer |
|---|---|---|
&self | Emprunt en lecture | Le cas courant : consulter sans modifier |
&mut self | Emprunt en écriture | Modifier l’instance en place |
self | Consommation | Conversions, chaînage de constructeurs, transformations définitives |
| (aucun) | Fonction associée | Constructeurs, fabriques, utilitaires liés au type |
Tip
Self, avec une majuscule, désigne le type courant à l’intérieur d’un bloc impl. L’employer plutôt que le nom réel évite d’avoir à modifier le corps du bloc lors d’un renommage.
Par convention, le constructeur principal s’appelle new — ici francisé en nouveau. Ce n’est pas un mot-clé, simplement un usage, et rien n’empêche d’en fournir plusieurs (from_str, with_capacity…).
Plusieurs blocs impl peuvent coexister pour un même type, ce qui permet de regrouper les méthodes par thème ou de séparer celles qui dépendent de contraintes génériques.
Les énumérations
Une énumération liste les valeurs possibles d’un type. Contrairement à ses équivalents dans beaucoup de langages, chaque variante peut transporter des données, de forme différente d’une variante à l’autre.
Les blocs impl s’appliquent aussi aux énumérations :
Le filtrage se fait par match, dont la syntaxe complète est traitée dans Déstructuration et patterns.
Rendre les états invalides irreprésentables
C’est l’usage le plus précieux des énumérations : encoder dans le type ce qu’une combinaison de champs ne peut qu’espérer.
Le compilateur devient alors un allié : l’exhaustivité du match garantit qu’aucun état n’est oublié, et l’ajout d’une variante fait échouer la compilation partout où le nouveau cas doit être traité.
Info
Option<T> et Result<T, E> ne sont rien d’autre que des énumérations de la bibliothèque standard, définies exactement selon ce principe. Rien n’empêche d’en écrire de semblables pour son propre domaine.
Les attributs derive
Les types définis ainsi n’ont, par défaut, presque aucun comportement : ils ne s’affichent pas, ne se comparent pas et ne se copient pas. L’attribut derive demande au compilateur de générer ces implémentations.
| Attribut | Ce qu’il apporte |
|---|---|
Debug | L’affichage de diagnostic {:?} — à mettre presque partout |
Clone | La duplication explicite par .clone() |
Copy | La copie implicite, réservée aux types entièrement sur la pile |
PartialEq, Eq | Les comparaisons == et != |
PartialOrd, Ord | Les comparaisons d’ordre et le tri |
Hash | L’emploi comme clé de HashMap ou HashSet |
Default | Une instance par défaut via Type::default() |
La dérivation n’est possible que si tous les champs la supportent : une structure contenant une String ne peut pas dériver Copy. Ces attributs ne sont que des implémentations de traits écrites pour vous, sujet développé dans Les traits.
Le patron constructeur
Rust n’a ni paramètres nommés, ni valeurs par défaut, ni surcharge de fonctions. Au-delà de trois ou quatre paramètres, le patron constructeur (builder) prend le relais, en s’appuyant sur les méthodes qui consomment self pour permettre le chaînage.
Visibilité
Tout est privé par défaut, y compris les champs d’une structure publique. Le mot-clé pub ouvre l’accès :
Garder les champs privés et exposer des méthodes permet de préserver les invariants du type — c’est ce qui rend possible, par exemple, la garantie qu’une String contient toujours de l’UTF-8 valide.
À retenir
- Les données (
struct,enum) et le comportement (impl) sont déclarés séparément. - Le receveur —
&self,&mut self,selfou rien — est le premier choix de conception d’une méthode. - Une énumération dont les variantes portent des données permet de rendre les états invalides irreprésentables.
derivefournit gratuitement les comportements courants ;Debugmérite d’être présent presque partout.- Il n’y a pas d’héritage : le partage de comportement passe par les traits.
Ressources
- Using Structs et Enums and Pattern Matching — chapitres du Rust Book
- Derivable traits — la liste complète des attributs dérivables
- Rust API Guidelines — les conventions de nommage des constructeurs et des accesseurs