Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Définir et instancier des structs

Les structs sont similaires aux tuples, abordés dans la section [« Le type tuple »][tuples], en ce sens que les deux contiennent plusieurs valeurs apparentées. Comme les tuples, les éléments d’une struct peuvent être de types différents. Contrairement aux tuples, dans une struct, vous nommerez chaque élément de données pour que la signification des valeurs soit claire. L’ajout de ces noms signifie que les structs sont plus flexibles que les tuples : vous n’avez pas besoin de vous fier à l’ordre des données pour spécifier ou accéder aux valeurs d’une instance.

Pour définir une struct, nous utilisons le mot-clé struct et nommons l’ensemble de la struct. Le nom d’une struct devrait décrire la signification des éléments de données regroupés. Ensuite, à l’intérieur d’accolades, nous définissons les noms et les types des éléments de données, que nous appelons des champs. Par exemple, le listing 5-1 montre une struct qui stocké des informations sur un compte utilisateur.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}
Listing 5-1: A User struct definition

Pour utiliser une struct après l’avoir définie, nous créons une instance de cette struct en spécifiant des valeurs concrètes pour chacun des champs. Nous créons une instance en indiquant le nom de la struct puis 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 que celui dans lequel nous les avons déclarés dans la struct. Autrement dit, la définition de la 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 le montre le listing 5-2.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}
Listing 5-2: Creating an instance of the User struct

Pour obtenir une valeur spécifique d’une struct, nous utilisons la notation par point. Par exemple, pour accéder à l’adresse e-mail de cet utilisateur, nous utilisons user1.email. Si l’instance est mutable, nous pouvons modifier une valeur en utilisant la notation par point et en assignant une valeur à un champ particulier. Le listing 5-3 montre comment modifier la valeur du champ email d’une instance mutable de User.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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");
}
Listing 5-3: Changing the value in the email field of a User instance

Notez que l’instance 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 struct comme dernière expression dans le corps de la fonction pour retourner implicitement cette nouvelle instance.

Le listing 5-4 montre une fonction build_user qui retourné une instance de User avec l’e-mail et le nom d’utilisateur donnés. Le champ active reçoit la valeur true, et sign_in_count reçoit la valeur 1.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}
Listing 5-4: A build_user function that takes an email and username and returns a User instance

Il est logique de nommer les paramètres de la fonction avec le même nom que les champs de la struct, mais devoir répéter les noms des champs email et username et les variables est un peu fastidieux. Si la struct avait plus de champs, répéter chaque nom deviendrait encore plus pénible. Heureusement, il existe un raccourci pratique !

Utiliser le raccourci d’initialisation de champ

Comme les noms des paramètres et les noms des champs de la struct sont exactement les mêmes dans le listing 5-4, nous pouvons utiliser la syntaxe du raccourci d’initialisation de champ pour réécrire build_user de manière à ce qu’elle se comporte exactement de la même façon mais sans la répétition de username et email, comme le montre le listing 5-5.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}
Listing 5-5: A build_user function that uses field init shorthand because the username and email parameters have the same name as struct fields

Ici, nous créons une nouvelle instance de la struct User, qui à un champ nommé email. Nous voulons définir la valeur du champ email à la valeur du paramètre email de la fonction build_user. Comme le champ email et le paramètre email portent le même nom, nous n’avons besoin d’écrire que email plutôt que email: email.

Créer des instances avec la syntaxe de mise à jour de struct

Il est souvent utile de créer une nouvelle instance d’une struct qui inclut la plupart des valeurs d’une autre instance du même type, mais en modifié certaines. Vous pouvez le faire en utilisant la syntaxe de mise à jour de struct.

D’abord, dans le listing 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 utilisons par ailleurs les mêmes valeurs de user1 que nous avons créé dans le listing 5-2.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}
Listing 5-6: Creating a new User instance using all but one of the values from user1

En utilisant la syntaxe de mise à jour de struct, nous pouvons obtenir le même effet avec moins de code, comme le montre le listing 5-7. La syntaxe .. spécifie que les champs restants non explicitement définis doivent avoir la même valeur que les champs de l’instance donnée.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}
Listing 5-7: Using struct update syntax to set a new email value for a User instance but to use the rest of the values from user1

Le code du listing 5-7 crée également une instance dans user2 qui à une valeur différente pour email mais qui conserve les mêmes valeurs pour les champs username, active et sign_in_count de user1. Le ..user1 doit apparaître en dernier pour spécifier que tous les champs restants doivent obtenir leurs valeurs des champs correspondants dans 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 de la struct.

section would apply. We can also still use user1.email in this example, because its value was not moved out of user1. –> Notez que la syntaxe de mise à jour de struct utilise = comme une assignation ; c’est parce qu’elle déplace les données, tout comme nous l’avons vu dans la section [« Les variables et les données interagissant avec le déplacement »][move]. Dans cet exemple, nous ne pouvons plus utiliser user1 après avoir créé user2 parce que la String du champ username de user1 a été déplacée dans user2. Si nous avions donné à user2 de nouvelles valeurs String pour email et username, et donc n’avions utilisé que les valeurs active et sign_in_count de user1, alors user1 serait toujours valide après la création de user2. active et sign_in_count sont tous deux des types qui implémentent le trait Copy, donc le comportement que nous avons abordé dans la section [« Données uniquement sur la pile : Copy »][copy] s’appliquerait. Nous pouvons également toujours utiliser user1.email dans cet exemple, car sa valeur n’a pas été déplacée hors de user1.

Créer des types différents avec les structs tuples

Rust prend également en charge des structs qui ressemblent à des tuples, appelées structs tuples. Les structs tuples ont la signification supplémentaire que le nom de la struct apporte, mais n’ont pas de noms associés à leurs champs ; elles n’ont que les types des champs. Les structs tuples sont utiles lorsque vous voulez donner un nom à l’ensemble du tuple et faire du tuple un type différent des autres tuples, et quand nommer chaque champ comme dans une struct classique serait verbeux ou redondant.

Pour définir une struct tuple, commencez par le mot-clé struct et le nom de la struct, suivis des types dans le tuple. Par exemple, ici nous définissons et utilisons deux structs tuples nommées Color et Point :

Filename: 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 types différents car ce sont des instances de structs tuples différentes. Chaque struct que vous définissez est son propre type, même si les champs de la 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 comme argument, même si les deux types sont composés de trois valeurs i32. Par ailleurs, les instances de structs tuples sont similaires aux tuples en ce que vous pouvez les déstructurer en leurs éléments individuels, et vous pouvez utiliser un . suivi de l’index pour accéder à une valeur individuelle. Contrairement aux tuples, les structs tuples exigent que vous nommiez le type de la struct lorsque vous les déstructurez. Par exemple, nous écririons let Point(x, y, z) = origin; pour déstructurer les valeurs du point origin dans des variables nommées x, y et z.

Définir des structs unitaires

Vous pouvez également définir des structs qui n’ont aucun champ ! Celles-ci sont appelées structs unitaires car elles se comportent de manière similaire à (), le type unitaire que nous avons mentionné dans la section [« Le type tuple »][tuples]. Les structs unitaires peuvent être utiles lorsque vous devez implémenter un trait sur un type mais que vous n’avez aucune donnée à stocker dans le type lui-même. Nous aborderons les traits au chapitre 10. Voici un exemple de déclaration et d’instanciation d’une struct unitaire nommée AlwaysEqual :

Filename: 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 ni de parenthèses ! Ensuite, nous pouvons obtenir une instance d’AlwaysEqual dans la variable subject de manière similaire : en utilisant le nom que nous avons défini, sans accolades ni parenthèses. Imaginez que plus tard nous implémenterons un comportement pour ce type de sorte que chaque instance d’AlwaysEqual soit toujours égale à chaque instance de tout autre type, peut-être pour avoir un résultat connu à des fins de test. Nous n’aurions 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 les structs unitaires.

Possession des données d’une struct

Dans la définition de la struct User du listing 5-1, nous avons utilisé le type possédé String plutôt que le type de slice de chaîne &str. C’est un choix délibéré car nous voulons que chaque instance de cette struct possède toutes ses données et que ces données soient valides aussi longtemps que la struct entière est valide.

Il est également possible pour les structs de stocker des références vers des données possédées par autre chose, mais cela nécessite l’utilisation de durées de vie (lifetimes), 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 une struct sont valides aussi longtemps que la struct l’est. Supposons que vous essayiez de stocker une référence dans une struct sans spécifier de durées de vie, comme dans le code suivant dans src/main.rs ; cela ne fonctionnera pas :

Filename: src/main.rs
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 se plaindra 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,
  |

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 expliquerons comment corriger ces erreurs de manière à pouvoir stocker des références dans des structs, mais pour l’instant, nous corrigerons des erreurs comme celles-ci en utilisant des types possédés comme String au lieu de références comme &str.