struct
Structures - Créer des types personnalisés avec Ce guide est actuellement en cours de rédaction : si quelque chose vous semble mal expliqué ou peu clair, n'hésitez pas à me faire un retour sur github ou par mail
Une nouvelle feuille sera publiée chaque semaine : si vous souhaitez être averti·e des nouveaux contenus, abonnez-vous par mail ou suivez-moi sur twitter
Déclarer une structure
Une structure est un type de données incontournable qui regroupe un ensemble de champs, dont chaque type est spécifié. On peut également y attacher des méthodes.
Voici comment déclarer une structure classique. On utilise le PascalCase pour le nommage des structures, au lieu de la snake_case habituelle :
// le trait debug est optionnel : il permet d'afficher
// une instance de notre structure avec `println!`
#[derive(Debug)]
struct User {
name: String,
email: String,
age: u8,
active: bool,
}
2
3
4
5
6
7
8
9
Pour utiliser concrètement une structure, on doit créer une instance :
fn main() {
let yann = User {
name: String::from("Yann"),
email: String::from("email@email.fr"),
age: 35,
active: true,
};
// afficher la valeur d'un champ
println!("age : {}", yann.age);
// afficher toute l'instance pour debug
println!("{:#?}", yann)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
unit struct
et tuple struct
Les structures sont souvent utilisées de deux autres manières qu'il est bon de savoir reconnaître.
On peut déclarer une structure sans aucun champ, on l'appelle alors structure unitaire (unit struct).
struct User;
On peut aussi créer des tuple struct
, qui fonctionnent exactement comme les tuple
, si ce n'est qu'ils ont un nom pour pouvoir être réutilisés. Supposons par exemple qu'on veuille réprésenter un point avec des coordonnées x
et y
; on pourrait utiliser un tuple struct
:
#[derive(Debug)]
struct Point(i32, i32);
fn main() {
let point = Point(0, 10);
// on accède aux valeurs de la même manière qu'avec
// un tuple classique : par leur index numérique
println!("{}", point.0);
println!("{}", point.1);
// affiche : Point(0, 10)
println!("{:?}", point)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Les structures unitaires et structures tuple permettent de comprendre la syntaxe des indispensables énumération en Rust, qui sont composées des 3 types de structures que l'on vient de voir. La seule différence est que le mot clef struct
n'est pas utilisé pour déclarer une variante d'une énumération :
enum Message {
Quit, // une structure unitaire
Move { x: i32, y: i32 }, // une structure classique
Write(String), // un structure tupple
ChangeColor(i32, i32, i32), // une structure tuple
}
2
3
4
5
6
structure mutables
Pour que les valeurs d'une instance de structure soient mutables, il faut rendre toute l'instance mutable en utilisant le mot clef mut au moment de l'instanciation de la structure :
let mut yann = User {
name: String::from("Yann"),
email: String::from("email@email.fr"),
age: 35,
active: true,
};
2
3
4
5
6
Muter les variables est désormais possible :
yann.age = 43;
yann.email = String::from("email@email.fr");
yann.active = false;
println!("debug : {:#?}", yann);
2
3
4
NOTE
Tous les champs de l'instance deviennent mutables, Rust n'autorise pas seulement certains champs à être mutables.
On peut utiliser des fonctions pour construire des instances avec des valeurs par défaut :
fn build_user(name: String, email: String) -> User {
User {
// notation abrégée. Identique à "name: name"
name,
// notation abrégée. Identique à "email: email"
email,
active: true,
age: 35,
}
}
2
3
4
5
6
7
8
9
10
Il est possible d'instancier une structure en se basant sur les valeurs d'une autre instance. On peut ainsi redéfinir uniquement certaines valeurs. L'exemple ci-dessous reprend toutes les valeurs de l'instance yann et redéfinit uniquement les clef name et email pour l'instance roger.
fn main() {
let yann = build_user(String::from("yann"), String::from("yann@yineo.fr"));
let roger = User {
// ces valeurs écrasent celle de l'instance "yann"
name: String::from("Roger"),
email: String::from("roger@roger.fr"),
// l'instance de base ( toujours à écrire en dernier )
..yann
};
println!("debug : {:#?}", roger);
}
2
3
4
5
6
7
8
9
10
11
Le debug ci-dessus affichera :
debug : User {
name: "Roger",
email: "roger@roger.fr",
age: 35,
active: true
}
2
3
4
5
6
Implémenter une méthode sur la structure
Une méthode est une fonction attachée à une structure, qui recoit automatiquement &self en premier argument ; qui est l'instance de la structure.
Pour ajouter une méthode, il faut créer un bloc impl. Voici comment définir une méthode area
sur une structure Rectangle
, qui calculera l'aire de l'instance du Rectangle :
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// ajout d'un bloc implémentation
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
2
3
4
5
6
7
8
9
10
11
12
Créons une instance de notre structure Rectangle sur laquelle on peut ensuite appeler notre méthode area
:
let my_rectangle = Rectangle {
width: 2,
height: 5,
};
let area_with_struct = my_rectangle.area();
2
3
4
5
Les fonctions associées
Les fonctions associées sont tout simplement des méthodes qui ne prennent pas &self
en premier paramètre.
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
2
3
4
5
6
7
8
Une fonction associée ne dépend pas des valeurs de l'instance : on l'appelle sans créer d'instance.
Rectangle::square(10);
On en sait maintenant assez pour comprendre la notation String::from("hello")
vu précédemment : String
est une structure Rust, et from
une fonction associée de la structure String
. Voici à quoi ressemble la déclaration de la structure String
:
pub struct String {
vec: Vec<u8>,
}
2
3
Elle contient un unique champ vec
qui représente une collection (Vec) d'octets, dont chacun représentera un caractère UTF-8.