Définir et instancier des structures
Les structures sont similaires aux tuples, qu'on a vus dans une section du chapitre 3, car tous les deux portent plusieurs valeurs associées. Comme pour les tuples, les éléments d'une structure peuvent être de différents types. Contrairement aux tuples, dans une structure on doit nommer chaque élément des données afin de clarifier le rôle de chaque valeur. L'ajout de ces noms font que les structures sont plus flexibles que les tuples : on n'a pas à utiliser l'ordre des données pour spécifier ou accéder aux valeurs d'une instance.
Pour définir une structure, on tape le mot-clé struct
et on donne un nom à
toute la structure. Le nom d'une structure devrait décrire l'utilisation des
éléments des données regroupés. Ensuite, entre des accolades, on définit le nom
et le type de chaque élément des données, qu'on appelle un champ. Par exemple,
l'encart 5-1 montre une structure qui stocke des informations à propos d'un
compte d'utilisateur.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn main() {}
Pour utiliser une structure après l'avoir définie, on crée une instance de
cette structure en indiquant des valeurs concrètes pour chacun des champs.
On crée une instance en indiquant le nom de la structure puis en ajoutant des
accolades qui contiennent des paires de clé: valeur
, où les clés sont les noms
des champs et les valeurs sont les données que l'on souhaite stocker dans ces
champs. Nous n'avons pas à préciser les champs dans le même ordre qu'on les a
déclarés dans la structure. En d'autres termes, la définition de la structure
décrit un gabarit pour le type, et les instances remplissent ce gabarit avec des
données précises pour créer des valeurs de ce type. Par exemple, nous pouvons
déclarer un utilisateur précis comme dans l'encart 5-2.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn main() { let utilisateur1 = Utilisateur { email: String::from("quelquun@example.com"), pseudo: String::from("pseudoquelconque123"), actif: true, nombre_de_connexions: 1, }; }
Pour obtenir une valeur spécifique depuis une structure, on utilise la notation
avec le point. Si nous voulions seulement l'adresse e-mail de cet utilisateur,
on pourrait utiliser utilisateur1.email
partout où on voudrait utiliser cette
valeur. Si l'instance est mutable, nous pourrions changer une valeur en
utilisant la notation avec le point et assigner une valeur à ce champ en
particulier. L'encart 5-3 montre comment changer la valeur du champ email
d'une instance mutable de Utilisateur
.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn main() { let mut utilisateur1 = Utilisateur { email: String::from("quelquun@example.com"), pseudo: String::from("pseudoquelconque123"), actif: true, nombre_de_connexions: 1, }; utilisateur1.email = String::from("unautremail@example.com"); }
À noter que l'instance tout entière doit être mutable ; Rust ne nous permet pas de marquer seulement certains champs comme mutables. Comme pour toute expression, nous pouvons construire une nouvelle instance de la structure comme dernière expression du corps d'une fonction pour retourner implicitement cette nouvelle instance.
L'encart 5-4 montre une fonction creer_utilisateur
qui retourne une instance
de Utilisateur
avec l'adresse e-mail et le pseudo fournis. Le champ actif
prend la valeur true
et le nombre_de_connexions
prend la valeur 1
.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn creer_utilisateur(email: String, pseudo: String) -> Utilisateur { Utilisateur { email: email, pseudo: pseudo, actif: true, nombre_de_connexions: 1, } } fn main() { let utilisateur1 = creer_utilisateur( String::from("quelquun@example.com"), String::from("pseudoquelconque123"), ); }
Il est logique de nommer les paramètres de fonction avec le même nom que les
champs de la structure, mais devoir répéter les noms de variables et de champs
email
et pseudo
est un peu pénible. Si la structure avait plus de champs,
répéter chaque nom serait encore plus fatigant. Heureusement, il existe un
raccourci pratique !
Utiliser le raccourci d'initialisation des champs
Puisque les noms des paramètres et les noms de champs de la structure sont
exactement les mêmes dans l'encart 5-4, on peut utiliser la syntaxe de
raccourci d'initialisation des champs pour réécrire creer_utilisateur
de
sorte qu'elle se comporte exactement de la même façon sans avoir à répéter
email
et pseudo
, comme le montre l'encart 5-5.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn creer_utilisateur(email: String, pseudo: String) -> Utilisateur { Utilisateur { email, pseudo, actif: true, nombre_de_connexions: 1, } } fn main() { let utilisateur1 = creer_utilisateur( String::from("quelquun@example.com"), String::from("pseudoquelconque123"), ); }
Ici, on crée une nouvelle instance de la structure Utilisateur
, qui possède
un champ nommé email
. On veut donner au champ email
la valeur du paramètre
email
de la fonction creer_utilisateur
. Comme le champ email
et le
paramètre email
ont le même nom, on a uniquement besoin d'écrire email
plutôt que email: email
.
Créer des instances à partir d'autres instances avec la syntaxe de mise à jour de structure
Il est souvent utile de créer une nouvelle instance de structure qui comporte la plupart des valeurs d'une autre instance tout en en changeant certaines. Vous pouvez utiliser pour cela la syntaxe de mise à jour de structure.
Tout d'abord, dans l'encart 5-6 nous montrons comment créer une nouvelle
instance de Utilisateur
dans utilisateur2
sans la syntaxe de mise à jour de
structure. On donne de nouvelles valeurs à email
et pseudo
mais on utilise
pour les autres champs les mêmes valeurs que dans utilisateur1
qu'on a créé à
l'encart 5-2.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn main() { // -- partie masquée ici -- let utilisateur1 = Utilisateur { email: String::from("quelquun@example.com"), pseudo: String::from("pseudoquelconque123"), actif: true, nombre_de_connexions: 1, }; let utilisateur2 = Utilisateur { actif: utilisateur1.actif, pseudo: utilisateur1.email, email: String::from("quelquundautre@example.com"), nombre_de_connexions: utilisateur1.nombre_de_connexions, }; }
En utilisant la syntaxe de mise à jour de structure, on peut produire le même
résultat avec moins de code, comme le montre l'encart 5-7. La syntaxe ..
indique que les autres champs auxquels on ne donne pas explicitement de valeur
devraient avoir la même valeur que dans l'instance précisée.
struct Utilisateur { actif: bool, pseudo: String, email: String, nombre_de_connexions: u64, } fn main() { // -- partie masquée ici -- let utilisateur1 = Utilisateur { email: String::from("quelquun@example.com"), pseudo: String::from("pseudoquelconque123"), actif: true, nombre_de_connexions: 1, }; let utilisateur2 = Utilisateur { email: String::from("quelquundautre@example.com"), ..utilisateur1 }; }
Le code dans l'encart 5-7 crée aussi une instance dans utilisateur2
qui a une
valeur différente pour email
, mais qui as les mêmes valeurs pour les champs
pseudo
, actif
et nombre_de_connexions
que utilisateur1
. Le
..utilisateur1
doit être inséré à la fin pour préciser que tous les champs
restants obtiendrons les valeurs des champs correspondants de utilisateur1
,
mais nous pouvons renseigner les valeurs des champs dans n'importe quel ordre,
peu importe leur position dans la définition de la structure.
Veuillez notez que la syntaxe de la mise à jour de structure utilise un =
comme le ferait une assignation ; car cela déplace les données, comme nous
l'avons vu dans une des sections au chapitre 4. Dans cet
exemple, nous ne pouvons plus utiliser utilisateur1
après avoir créé
utilisateur2
car la String
dans le champ pseudo
de utilisateur1
a été
déplacée dans utilisateur2
. Si nous avions donné des nouvelles valeurs pour
chacune des String
email
et pseudo
, et que par conséquent nous aurions
déplacé uniquement les valeurs de actif
et de nombre_de_connexions
à partir
de utilisateur1
, alors utilisateur1
restera en vigueur après avoir créé
utilisateur2
. Les types de actif
et de nombre_de_connexions
sont de types
qui implémentent le trait Copy
, donc le comportement décris dans la section
à propos de copy aura lieu ici.
Utilisation de structures tuples sans champ nommé pour créer des types différents
Rust prend aussi en charge des structures qui ressemblent à des tuples, appelées structures tuples. La signification d'une structure tuple est donnée par son nom. En revanche, ses champs ne sont pas nommés ; on ne précise que leurs types. Les structures tuples servent lorsqu'on veut donner un nom à un tuple pour qu'il soit d'un type différent des autres tuples, et lorsque nommer chaque champ comme dans une structure classique serait trop verbeux ou redondant.
La définition d'une structure tuple commence par le mot-clé struct
et le nom
de la structure suivis des types des champs du tuple. Par exemple ci-dessous,
nous définissons et utilisons deux structures tuples nommées Couleur
et
Point
:
struct Couleur(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let noir = Couleur(0, 0, 0); let origine = Point(0, 0, 0); }
Notez que les valeurs noir
et origine
sont de types différents parce que ce
sont des instances de structures tuples différentes. Chaque structure que l'on
définit constitue son propre type, même si les champs au sein de la structure
ont les mêmes types. Par exemple, une fonction qui prend un paramètre de type
Couleur
ne peut pas prendre un argument de type Point
à la place, bien que
ces deux types soient tous les deux constitués de trois valeurs i32
. Mis à
part cela, les instances de stuctures tuples se comportent comme des tuples : on
peut les déstructurer en éléments individuels, on peut utiliser un .
suivi de
l'indice pour accéder individuellement à une valeur, et ainsi de suite.
Les structures unité sans champs
On peut aussi définir des structures qui n'ont pas de champs ! Cela s'appelle
des structures unité parce qu'elles se comportent d'une façon analogue au type
unité, ()
, que nous avons vu dans la section sur les
tuples. Les structures unité sont utiles lorsqu'on doit
implémenter un trait sur un type mais qu'on n'a aucune donnée à stocker dans le
type en lui-même. Nous aborderons les traits au chapitre 10. Voici un exemple
de déclaration et d'instanciation d'une structure unité ToujoursEgal
:
struct ToujoursEgal; fn main() { let sujet = ToujoursEgal; }
Pour définir ToujoursEgal
, nous utilisons le mot-clé struct
, puis le nom que
nous voulons lui donner, et enfin un point-virgule. Pas besoin d'accolades ou de
parenthèses ! Ensuite, nous pouvons obtenir une instance de ToujourEgal
dans
la variable sujet
de la même manière : utilisez le nom que vous avez défini,
sans aucune accolade ou parenthèse. Imaginez que plus tard nous allons
implémenter un comportement pour ce type pour que toutes les instances de
ToujourEgal
soient toujours égales à chaque instance de n'importe quel autre
type, peut-être pour avoir un résultat connu pour des besoins de tests. Nous
n'avons besoin d'aucune donnée pour implémenter ce comportement ! Vous verrez
au chapitre 10 comment définir des traits et les implémenter sur n'importe quel
type, y compris sur les structures unité.
La possession des données d'une structure
Dans la définition de la structure
Utilisateur
de l'encart 5-1, nous avions utilisé le type possédéString
plutôt que le type de slice de chaîne de caractères&str
. Il s'agit d'un choix délibéré puisque nous voulons que chacune des instances de cette structure possèdent toutes leurs données et que ces données restent valides tant que la structure tout entière est valide.Il est aussi possible pour les structures de stocker des références vers des données possédées par autre chose, mais cela nécessiterait d'utiliser des durées de vie, une fonctionnalité de Rust que nous aborderons au chapitre 10. Les durées de vie assurent que les données référencées par une structure restent valides tant que la structure l'est aussi. Disons que vous essayiez de stocker une référence dans une structure sans indiquer de durées de vie, comme ce qui suit, ce qui ne fonctionnera pas :
Fichier : src/main.rs
struct Utilisateur { actif: bool, pseudo: &str, email: &str, nombre_de_connexions: u64, } fn main() { let utilisateur1 = Utilisateur { email: "quelquun@example.com", pseudo: "pseudoquelconque123", actif: true, nombre_de_connexions: 1, }; }
Le compilateur réclamera l'ajout des durées de vie :
$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | pseudo: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct Utilisateur<'a> { 2 | actif: bool, 3 ~ pseudo: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct Utilisateur<'a> { 2 | actif: bool, 3 | pseudo: &str, 4 ~ email: &'a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` due to 2 previous errors
Au chapitre 10, nous aborderons la façon de corriger ces erreurs pour qu'on puisse stocker des références dans des structures, mais pour le moment, nous résoudrons les erreurs comme celles-ci en utilisant des types possédés comme
String
plutôt que des références comme&str
.