Aller au contenu

Les structs en Rust

Une struct (structure) regroupe des valeurs liées entre elles dans un seul type nommé. Là où un tuple est un petit paquet rapide et anonyme, une struct donne un nom à chaque champ, ce qui rend le code qui l’utilise bien plus facile à lire.

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Alice"),
age: 30,
};
println!("{} a {} ans", alice.name, alice.age);
}

Sortie :

Alice a 30 ans
▶ Essayez par vous-même

Comparez avec un tuple ("Alice", 30) : avec une struct, alice.name et alice.age se comprennent tout seuls. Vous n’avez pas besoin de vous rappeler que le champ 0 est le nom et le champ 1 l’âge.

Comme les variables, les instances de struct sont immuables par défaut. Ajoutez mut pour pouvoir modifier un champ après la création :

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Alice"),
age: 30,
};
alice.age += 1;
println!("{} a maintenant {} ans", alice.name, alice.age);
}
▶ Essayez par vous-même

Vous pouvez attacher des fonctions à une struct avec un bloc impl. À l’intérieur, &self désigne l’instance précise sur laquelle la méthode a été appelée :

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("Bonjour, je m'appelle {} et j'ai {} ans", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
alice.greet();
}

Sortie :

Bonjour, je m'appelle Alice et j'ai 30 ans
▶ Essayez par vous-même

Un motif courant consiste à écrire une fonction new dans le bloc impl qui construit une instance pour vous. Ainsi, ceux qui l’appellent n’ont pas à écrire chaque champ à la main :

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Bob", 25);
println!("{} a {} ans", bob.name, bob.age);
}
▶ Essayez par vous-même

Remarquez age tout seul dans Person { name: name.to_string(), age } : quand une variable porte exactement le même nom que le champ, vous pouvez éviter d’écrire age: age et écrire simplement age. Rust appelle cela le field init shorthand (raccourci d’initialisation des champs).

Essayez println!("{:?}", alice) sur la struct telle qu’elle est écrite plus haut : ça ne compile pas. Rust ne sait pas afficher un type personnalisé en mode debug tant que vous ne le lui demandez pas. Ajoutez #[derive(Debug)] au-dessus de la struct pour l’obtenir gratuitement :

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
println!("{:?}", alice);
}

Sortie :

Person { name: "Alice", age: 30 }
▶ Essayez par vous-même

#[derive(...)] est la façon dont Rust génère automatiquement des fonctionnalités courantes pour un type. Vous verrez sans arrêt Debug, et plus tard Clone et d’autres, ajoutés de cette manière.

Créez une struct Rectangle avec des champs width (largeur) et height (hauteur) de type u32. Donnez-lui deux méthodes : area, qui renvoie l’aire, et is_square, qui renvoie true quand les deux côtés sont égaux. Indice : les deux méthodes ne font que lire les champs, elles prennent donc &self.

Voir la solution
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("large : aire {}, carré ? {}", wide.area(), wide.is_square());
println!("carré : aire {}, carré ? {}", square.area(), square.is_square());
}

Sortie :

large : aire 24, carré ? false
carré : aire 25, carré ? true
▶ Essayez par vous-même

Créez une struct Account avec un owner (titulaire, une String) et un balance (solde) en centimes (u32). Écrivez Account::new("Ana"), qui démarre avec un solde de 0. Déposez ensuite 50 puis 25 centimes en modifiant le champ, et affichez le compte de deux façons : avec une phrase normale et avec {:?}. Indice : la variable doit être déclarée avec let mut pour que vous puissiez modifier account.balance, et {:?} a besoin de #[derive(Debug)].

Voir la solution
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Ana");
account.balance += 50;
account.balance += 25;
println!("{} a {} centimes", account.owner, account.balance);
println!("{:?}", account);
}

Sortie :

Ana a 75 centimes
Account { owner: "Ana", balance: 75 }
▶ Essayez par vous-même

new fixe lui-même balance: 0, donc personne ne peut l’oublier en l’appelant. C’est l’une des grandes raisons pour lesquelles les fonctions constructeurs sont si courantes.

Mettez trois valeurs Person dans un vecteur : Ana (34 ans), Ben (16 ans) et Chen (52 ans). Affichez le nom de toutes les personnes de 18 ans ou plus, puis affichez qui est la plus âgée. Indice : parcourez la liste avec for person in &people, et retenez la personne la plus âgée trouvée jusqu’ici avec let mut oldest = &people[0];.

Voir la solution
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Ana"), age: 34 },
Person { name: String::from("Ben"), age: 16 },
Person { name: String::from("Chen"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{} est adulte", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("La personne la plus âgée : {} ({} ans)", oldest.name, oldest.age);
}

Sortie :

Ana est adulte
Chen est adulte
La personne la plus âgée : Chen (52 ans)
▶ Essayez par vous-même

oldest est une référence (&Person) vers un élément du vecteur, pas une copie : rien n’est cloné. Chaque fois que nous trouvons quelqu’un de plus âgé, nous faisons simplement pointer oldest vers cette personne.

  • Une struct regroupe des champs nommés dans un seul type : struct Person { name: String, age: u32 }.
  • Créez une instance avec Person { name: ..., age: ... } ; déclarez-la mut pour pouvoir modifier ses champs ensuite.
  • Ajoutez du comportement avec un bloc impl ; &self désigne l’instance sur laquelle la méthode a été appelée.
  • Ajoutez #[derive(Debug)] pour pouvoir afficher une struct avec {:?}.

Petit quiz

1. Quel est le principal avantage d'une struct par rapport à un tuple ?

2. Qu'est-ce que l'ajout de #[derive(Debug)] à une struct vous permet de faire ?

3. Dans Person { name: name.to_string(), age }, que fait le age tout seul ?

Score : 0 / 3