Aller au contenu

Les structs en Rust : des supers variables

Rust n’est pas un langage orienté objet, donc on ne peut pas utiliser ni d’objet ni de classes.

À la place on va avoir ce qu’on appelle des structs, c’est un genre de variable qui va pouvoir contenir plusieurs champs de types différents.

Ce qu’il faut bien comprendre c’est que lorsqu’on crée un struct on crée un type de données par la même occasion.

  • Un struct peut contenir plusieurs champs de types différents
  • Un struct doit être créé dans le global scope

Création et Initialisation

Voici comment on définit un struct :

struct User {
first_name: String,
last_name: String,
age: u8
}

Maintenant voici comment on crée une instance de type User:

struct User {
first_name: String,
last_name: String,
age: u8,
}
fn main() {
let user_one = User { first_name: String::from("Bob"), last_name: String::from("L'éponge"), age: 22 };
}

Si on veut print notre variable user_one il va falloir ajouter le trait Debug à notre struct de cette façon :

#[derive(Debug)] // permet de print le struct
struct User {
first_name: String,
last_name: String,
age: u8,
}
fn main() {
let user_one = User { first_name: String::from("Bob"), last_name: String::from("L'éponge"), age: 22 };
println!("{:?}", user_one); // on utilise :? pour utiliser la fonction debug pour print l'élément en question
}

J’expliquerai dans un prochain article ce qu’est un trait, mais pour l’instant ce n’est pas grave si tu ne sais pas ce que c’est.

Modifier une instance de struct

Comme une instance de struct est une variable, si on la modifie il va falloir la déclarer mutable, si elle est mutable alors tous les champs sont mutables, on ne peut pas définir que certains champs mutables.

struct User {
first_name: String,
last_name: String,
age: u8,
}
fn main() {
let mut user_one = User { first_name: String::from("Bob"), last_name: String::from("L'éponge"), age: 22 };
user_one.first_name = String::from("Bernie");
}

Tu vois qu’ici on a pu modifier le champ first_name de user_one avec la string “Bernie” car la variable user_one est mutable.

Les Méthodes

Les structs peuvent avoir des fonctions utilisables par une instance d’un struct, c’est ce qu’on appelle des méthodes.

Pour définir des méthodes on va d’abord utiliser le mot clé impl qui signifie “implémenter” suivi du nom du struct, puis on va déclarer les méthodes dans le bloc en question.

struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rec = Rectangle { width: 20, height: 10 };
let rec_area = rec.area();
}

La syntaxe pour appeler une méthode est donc le nom de l’instance du struct suivi d’un . puis le nom de la méthode.

Tu vois qu’ici on vient de créer le bloc impl Rectangle qui contient une seule méthode, la méthode area.

Tu remarques que cette méthode prend en paramètre &self, c’est-à-dire une référence de notre instance de struct.

On peut se demander pourquoi une référence ? Eh bien tout simplement parce que si on n’avait pas utilisé de référence notre instance de struct serait drop après avoir lancé la méthode area.

C’est à cause des règles de l’ownership, notre struct aurait été move car il serait passé implicitement en paramètre de la fonction area

Et oui quand on fait rec.area() en réalité on lance la méthode area avec rec en paramètre !

La syntaxe rec.area() est simplement du sucre syntaxique pour rendre plus lisible notre code, et donc étant move dans area il serait drop à la fin de area.

Fonctions associées

Les fonctions associées sont des fonctions qui se trouvent dans un bloc impl d’un struct mais qui n’ont pas besoin d’une instance de struct pour être lancés.

Donc les fonctions associées ne prendront pas &self ou self en paramètre.

Voici à quoi ressemble des fonctions associées :

struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
fn main() {
let square = Rectangle::square(10);
}

Tu vois que la syntaxe pour appeler des fonctions associées est un peu spéciale. On utilise le nom du struct suivi de :: puis le nom de la fonction associée.

Les méthodes, quant à elles, sont appelées avec le ., par exemple mon_rectangle.area() dans l’exemple précédent

Raccourci d’initialisation d’un struct

Rust nous donne un raccourci pour créer un struct sans se répéter.

Si on veut créer un struct avec des variables qui ont le même nom que les champs on n’a pas besoin de spécifier les champs.

struct User {
email: String,
username: String
}
fn main() {
let email = String::from("john@doe.com");
let username = String::from("jojodoe");
let user = User {
email, // Rust va automatiquement remplacer par email: email
username // Rust va automatiquement remplacer par username: username
};
}

Comme les variables ont le même nom que les champs, on peut uniquement mettre le nom de la variable et Rust va l’assigner automatiquement au champ du même nom.

Raccourci de modification d’un struct

Un deuxième raccourci bien pratique est disponible pour les structs.

Il s’agit du raccourci de modification, cela nous permet de créer un struct en reprenant tous les champs ou seulement certains d’un autre struct.

struct User {
email: String,
username: String,
first_name: String,
}
fn main() {
let user_one = User { email: String::from("john@doe.com"), username: String::from("JohnDoe"), first_name: String::from("John") };
let user_two = User { email: String::from("jacki@chan.com"), ..user_one };
}

Tu vois qu’on a créé un user_two en reprenant tous les champs de user_one SAUF le champ email que l’on a redéfini explicitement grâce à la syntaxe ..mon_autre_struct

Ce raccourci est un gros grain de temps quand on a des structs avec beaucoup de champs et il est donc très pratique.