Structures - Créer des types personnalisés avec struct

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,
}
1
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)
}
1
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;
1

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)
}
1
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
}
1
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,
};
1
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);
1
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,
    }
}
1
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);
}
1
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
}
1
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
    }
}
1
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();
1
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,
        }
    }
}
1
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);
1

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>,
}
1
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.