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 fait 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() {}
User struct definitionPour 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 que nous les avons 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 {
actif: true,
pseudo: String::from("pseudoquelconque123"),
email: String::from("quelquun@example.com"),
nombre_de_connexions: 1,
};
}
User structPour obtenir une valeur spécifique depuis une structure, on utilise la notation avec le point. Par exemple, pour récupérer l’adresse électronique de cet utilisateur, nous utilisons utilisateur1.email. 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 {
actif: true,
pseudo: String::from("pseudoquelconque123"),
email: String::from("quelquun@example.com"),
nombre_de_connexions: 1,
};
utilisateur1.email = String::from("unautremail@example.com");
}
email field of a User instanceÀ 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 {
actif: true,
pseudo: pseudo,
email: email,
nombre_de_connexions: 1,
}
}
fn main() {
let utilisateur1 = creer_utilisateur(
String::from("quelquun@example.com"),
String::from("pseudoquelconque123"),
);
}
build_user function that takes an email and username and returns a User instanceIl 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 pseudo et email, 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 {
actif: true,
pseudo,
email,
nombre_de_connexions: 1,
}
}
fn main() {
let utilisateur1 = creer_utilisateur(
String::from("quelquun@example.com"),
String::from("pseudoquelconque123"),
);
}
build_user function that uses field init shorthand because the username and email parameters have the same name as struct fieldsIci, 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 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 du même type, 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 de manière classique, sans utiliser la syntaxe de mise à jour de structure. On donne une nouvelle valeur au champ email 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,
};
}
User instance using all but one of the values from user1En 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
};
}
email value for a User instance but to use the rest of the values from user1Le code dans l’encart 5-7 crée aussi une instance dans utilisateur2 qui a une valeur différente pour email, mais qui a 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 obtiendront 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 mise à jour d’une structure utilise un = comme c’est le cas pour une assignation ; c’est parce que 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 resterait en vigueur après la création de utilisateur2. Les types de actif et de nombre_de_connexions implémentent tous deux le trait Copy, donc le comportement décrit dans la section à propos de copy aura lieu ici. Nous pouvons toujours utiliser utilisateur1.email dans cet exemple, car sa valeur n’a pas été déplacée hors de utilisateur1.
Création de différents types avec les structures tuples
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 vous définissez constitue un type à part entière, même si les champs au sein de la structure peuvent être du même type. 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 sont similaires aux tuples, en ce sens que vous pouvez les déstructurer en leurs éléments individuels, et vous pouvez utiliser un . suivi de l’indice pour accéder individuellement à une valeur, et ainsi de suite. Contrairement aux tuples, les structures tuples exigent que vous nommiez le type de la structure lorsque vous les déstructurez. Par exemple, on écrirait let Point(x, y, z) = origin; pour déstructurer les valeurs du point origin en variables nommées x, y et z.
Définition de structures unité
On peut aussi définir des structures qui n’ont pas de champ ! 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ède ses 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 essayez de stocker une référence dans une structure sans indiquer de durée de vie, comme ce qui suit, cela ne fonctionnera pas :
struct Utilisateur {
actif: bool,
pseudo: &str,
email: &str,
nombre_de_connexions: u64,
}
fn main() {
let utilisateur1 = Utilisateur {
actif: true,
pseudo: "pseudoquelconque123",
email: "quelquun@example.com",
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` (bin "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.