Introduction
Bienvenue dans Définir et instancier des structs. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.
Dans ce laboratoire, nous allons apprendre à définir et à instancier des structs en Rust, où les structs contiennent plusieurs valeurs liées et peuvent avoir des champs nommés, permettant une utilisation et un accès plus flexibles des données.
Définir et instancier des structs
Les structs sont similaires aux tuples, discutés dans "Le type tuple", en ce sens qu'ils contiennent tous deux plusieurs valeurs liées. Comme les tuples, les éléments d'un struct peuvent être de différents types. Contrairement aux tuples, dans un struct, vous allez nommer chaque élément de données pour qu'il soit clair ce que signifient les valeurs. Ajouter ces noms signifie que les structs sont plus flexibles que les tuples : vous n'avez pas à vous fier à l'ordre des données pour spécifier ou accéder aux valeurs d'une instance.
Pour définir un struct, nous entrons le mot clé struct et nommons l'ensemble du struct. Le nom d'un struct devrait décrire l'importance des éléments de données regroupés. Ensuite, à l'intérieur des accolades, nous définissons les noms et les types des éléments de données, que nous appelons champs. Par exemple, la Liste 5-1 montre un struct qui stocke des informations sur un compte utilisateur.
Nom de fichier : src/main.rs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Liste 5-1 : Définition d'un struct User
Pour utiliser un struct après l'avoir défini, nous créons une instance de ce struct en spécifiant des valeurs concrètes pour chacun des champs. Nous créons une instance en indiquant le nom du struct et en ajoutant des accolades contenant des paires clé : valeur, où les clés sont les noms des champs et les valeurs sont les données que nous voulons stocker dans ces champs. Nous n'avons pas besoin de spécifier les champs dans le même ordre dans lequel nous les avons déclarés dans le struct. En d'autres termes, la définition du struct est comme un modèle général pour le type, et les instances remplissent ce modèle avec des données particulières pour créer des valeurs du type. Par exemple, nous pouvons déclarer un utilisateur particulier comme indiqué dans la Liste 5-2.
Nom de fichier : src/main.rs
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
Liste 5-2 : Création d'une instance du struct User
Pour obtenir une valeur spécifique d'un struct, nous utilisons la notation point. Par exemple, pour accéder à l'adresse e-mail de cet utilisateur, nous utilisons user1.email. Si l'instance est mutable, nous pouvons changer une valeur en utilisant la notation point et en assignant à un champ particulier. La Liste 5-3 montre comment changer la valeur dans le champ email d'une instance mutable de User.
Nom de fichier : src/main.rs
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
Liste 5-3 : Changement de la valeur dans le champ email d'une instance de User
Notez que l'ensemble de l'instance doit être mutable ; Rust ne nous permet pas de marquer seulement certains champs comme mutables. Comme avec toute expression, nous pouvons construire une nouvelle instance du struct comme dernière expression dans le corps de la fonction pour retourner implicitement cette nouvelle instance.
La Liste 5-4 montre une fonction build_user qui renvoie une instance de User avec l'e-mail et le nom d'utilisateur donnés. Le champ active obtient la valeur true, et le sign_in_count obtient une valeur de 1.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Liste 5-4 : Une fonction build_user qui prend un e-mail et un nom d'utilisateur et renvoie une instance de User
Il est logique de nommer les paramètres de la fonction avec le même nom que les champs du struct, mais devoir répéter les noms de champs email et username et les variables est un peu fastidieux. Si le struct avait plus de champs, répéter chaque nom serait encore plus ennuyeux. Heureusement, il y a un raccourci pratique!
Utiliser la syntaxe raccourcie d'initialisation de champ
Dans la Liste 5-4, les noms des paramètres et des champs du struct étant exactement les mêmes, nous pouvons utiliser la syntaxe raccourcie d'initialisation de champ pour réécrire build_user de manière à ce qu'elle se comporte exactement de la même manière mais sans répéter username et email, comme indiqué dans la Liste 5-5.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
Liste 5-5 : Une fonction build_user qui utilise la syntaxe raccourcie d'initialisation de champ car les paramètres username et email ont le même nom que les champs du struct
Ici, nous créons une nouvelle instance du struct User, qui a un champ nommé email. Nous voulons définir la valeur du champ email sur la valeur du paramètre email de la fonction build_user. Comme le champ email et le paramètre email ont le même nom, nous n'avons qu'à écrire email au lieu de email: email.
Création d'instances à partir d'autres instances avec la syntaxe de mise à jour de struct
Il est souvent utile de créer une nouvelle instance d'un struct qui inclut la plupart des valeurs d'une autre instance, mais en modifie quelques-unes. Vous pouvez le faire en utilisant la syntaxe de mise à jour de struct.
Tout d'abord, dans la Liste 5-6, nous montrons comment créer une nouvelle instance de User dans user2 de manière classique, sans la syntaxe de mise à jour. Nous définissons une nouvelle valeur pour email, mais sinon, nous utilisons les mêmes valeurs que celles de user1 que nous avons créées dans la Liste 5-2.
Nom de fichier : src/main.rs
fn main() {
--snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
Liste 5-6 : Création d'une nouvelle instance de User en utilisant l'une des valeurs de user1
En utilisant la syntaxe de mise à jour de struct, nous pouvons obtenir le même effet avec moins de code, comme indiqué dans la Liste 5-7. La syntaxe .. spécifie que les autres champs non explicitement définis doivent avoir la même valeur que les champs de l'instance donnée.
Nom de fichier : src/main.rs
fn main() {
--snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
Liste 5-7 : Utilisation de la syntaxe de mise à jour de struct pour définir une nouvelle valeur pour email dans une instance de User, mais pour utiliser les autres valeurs de user1
Le code de la Liste 5-7 crée également une instance dans user2 qui a une valeur différente pour email, mais qui a les mêmes valeurs pour les champs username, active et sign_in_count de user1. Le ..user1 doit être placé en dernier pour spécifier que tous les autres champs doivent prendre leurs valeurs à partir des champs correspondants de user1, mais nous pouvons choisir de spécifier des valeurs pour autant de champs que nous le souhaitons dans n'importe quel ordre, indépendamment de l'ordre des champs dans la définition du struct.
Notez que la syntaxe de mise à jour de struct utilise = comme une affectation ; c'est parce qu'elle déplace les données, tout comme nous l'avons vu dans "Variables et données interagissant avec Move". Dans cet exemple, nous ne pouvons plus utiliser user1 après avoir créé user2 car la String dans le champ username de user1 a été déplacée dans user2. Si nous avions donné de nouvelles valeurs de String pour email et username dans user2, et donc seulement utilisé les valeurs active et sign_in_count de user1, alors user1 serait toujours valide après la création de user2. Tant active que sign_in_count sont des types qui implémentent le trait Copy, donc le comportement que nous avons discuté dans "Données uniquement sur la pile : Copy" s'appliquerait.
Utiliser des structs tuple sans champs nommés pour créer différents types
Rust prend également en charge des structs qui ressemblent aux tuples, appelés structs tuple. Les structs tuple ont la signification supplémentaire que le nom du struct apporte, mais n'ont pas de noms associés à leurs champs ; au lieu de cela, ils ont simplement les types des champs. Les structs tuple sont utiles lorsque vous voulez donner un nom à l'ensemble du tuple et que vous voulez que le tuple soit un type différent des autres tuples, et lorsque nommer chaque champ comme dans un struct classique serait verbeux ou redondant.
Pour définir un struct tuple, commencer par le mot clé struct et le nom du struct suivi des types dans le tuple. Par exemple, ici nous définissons et utilisons deux structs tuple nommés Color et Point :
Nom de fichier : src/main.rs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Notez que les valeurs black et origin sont de différents types car elles sont des instances de différents structs tuple. Chaque struct que vous définissez est un type propre, même si les champs à l'intérieur du struct peuvent avoir les mêmes types. Par exemple, une fonction qui prend un paramètre de type Color ne peut pas prendre un Point en argument, même si les deux types sont composés de trois valeurs i32. Sinon, les instances de struct tuple sont similaires aux tuples dans le sens où vous pouvez les décomposer en leurs parties individuelles, et vous pouvez utiliser un . suivi de l'index pour accéder à une valeur individuelle.
Structs sans champs, ressemblant à l'unité
Vous pouvez également définir des structs qui n'ont aucun champ! Ce sont appelés structs ressemblant à l'unité car ils se comportent de manière similaire à (), le type unité que nous avons mentionné dans "Le type tuple". Les structs ressemblant à l'unité peuvent être utiles lorsque vous devez implémenter un trait sur un certain type mais n'avez pas de données que vous souhaitiez stocker dans le type lui-même. Nous aborderons les traits au Chapitre 10. Voici un exemple de déclaration et d'instanciation d'un struct unité nommé AlwaysEqual :
Nom de fichier : src/main.rs
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Pour définir AlwaysEqual, nous utilisons le mot clé struct, le nom que nous souhaitons, puis un point-virgule. Pas besoin d'accolades ou de parenthèses! Ensuite, nous pouvons obtenir une instance de AlwaysEqual dans la variable subject de manière similaire : en utilisant le nom que nous avons défini, sans aucune accolade ou parenthèse. Imaginez que plus tard, nous implémentons un comportement pour ce type de sorte que chaque instance de AlwaysEqual soit toujours égale à chaque instance de n'importe quel autre type, peut-être pour avoir un résultat connu à des fins de test. Nous n'aurions pas besoin de données 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 les structs ressemblant à l'unité.
Propriété des données des structs
Dans la définition du struct
Userdans la Liste 5-1, nous avons utilisé le typeStringpropriétaire plutôt que le type de tranche de chaîne&str. C'est un choix délibéré car nous voulons que chaque instance de ce struct ait la propriété de toutes ses données et que ces données soient valides aussi longtemps que l'ensemble du struct est valide.Il est également possible pour les structs de stocker des références à des données appartenant à autre chose, mais pour ce faire, il est nécessaire d'utiliser des durées de vie, une fonctionnalité de Rust que nous aborderons au Chapitre 10. Les durées de vie garantissent que les données référencées par un struct sont valides aussi longtemps que le struct est valide. Disons que vous essayez de stocker une référence dans un struct sans spécifier de durées de vie, comme suit dans
src/main.rs; cela ne fonctionnera pas :struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }Le compilateur signalera qu'il a besoin de spécificateurs de durée de vie :
$ `cargo run` Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'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 User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, |Au Chapitre 10, nous aborderons comment corriger ces erreurs pour que vous puissiez stocker des références dans des structs, mais pour l'instant, nous corrigerons des erreurs comme celles-ci en utilisant des types propriétaires comme
Stringau lieu de références comme&str.
Sommaire
Félicitations! Vous avez terminé le laboratoire sur la définition et l'instanciation de structs. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.