Blog ENI : Toute la veille numérique !
-25€ dès 75€ sur les livres en ligne, vidéos... avec le code FUSEE25. J'en profite !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Rust
  3. Les traits prédéfinis en Rust
Extrait - Rust Développez des programmes robustes et sécurisés
Extraits du livre
Rust Développez des programmes robustes et sécurisés
4 avis
Revenir à la page d'achat du livre

Les traits prédéfinis en Rust

Introduction

Nous avons abordé la notion de trait dans le chapitre Structures en Rust, dans lequel nous avons pu introduire la notion de traits prédéfinis. Dans le chapitre Les traits en Rust, nous avons ensuite pu apprendre à créer nos propres traits et à les manipuler dans diverses situations.

Dans le présent chapitre, nous allons aller plus loin dans la notion de trait, en approfondissant la notion de trait prédéfini, c’est-à-dire ceux qui existent déjà dans la librairie standard de Rust notamment. Nous verrons comment les manipuler et les utiliser au mieux. Nous en profiterons pour présenter les plus connus et utiles au-delà de Display, Debug, Clone et Copy, déjà évoqués.

Des traits prédéfinis essentiels : les itérateurs

Définitions

Un itérateur est un objet qui permet de parcourir tous les éléments d’une collection (une liste par exemple). Nous aurons l’occasion d’en utiliser souvent dans le chapitre ultérieur dédié aux collections, Autres collections en langage Rust.

À travers sa librairie standard, le langage C++ propose l’usage d’un itérateur implicite.

En langage C#, il existe une interface IEnumerator qu’il s’agit d’implémenter pour pouvoir obtenir un itérateur :

interface IEnumerator { 
    void Reset(); 
    bool MoveNext(); 
    object Current { get; } 
} 

On est plus ou moins dans la même situation qu’en C#, en langage Rust, avec l’existence d’un trait prédéfini nommé :

pub trait Iterator { 
    type Item; 
 
    fn next(&mut self) -> Option<Self::Item>; 
} 

Ce trait possède d’autres méthodes dont il fournit l’implémentation. Une seule méthode n’est pas implémentée, c’est à l’entité qui l’implémente de le faire : la méthode next, qui permet de passer...

Notion de surcharge d’opérateurs

1. Introduction

Nous avons déjà abordé la notion de surcharge de méthodes, lorsque l’on a opéré la surcharge d’une méthode dans une structure implémentant un trait fournissant une implémentation de ladite méthode.

La surcharge d’opérateurs montre quelques similarités avec ce scénario. En effet, on peut définir ainsi respectivement un opérateur et une surcharge d’opérateur :

  • Un opérateur est une fonction spéciale. Le caractère associé est réservé dans le langage. Pour l’essentiel, il s’agit d’opérateurs mathématiques comme +, ==, -, /, %, etc.

  • La surcharge des opérateurs consiste à fournir une surcharge de l’implémentation de cet opérateur, pour un type donné. C’est ni plus ni moins qu’une surcharge de méthode, mais avec une méthode qui est associée à un caractère « mathématique » spécial et réservé.

2. Les opérateurs surchargeables

Il existe plusieurs catégories d’opérateurs surchargeables. Pour chacun, on indique son nom, sa caisse et son module de rattachement. Par ailleurs, la documentation suivante rappelle certains de ces opérateurs surchargeables : https://doc.rust-lang.org/book/appendix-02-operators.html

Opérateurs logiques

-valeur : valeur négative : std::ops::Neg 
!valeur : valeur opposée : std::ops:Not 

Opérateurs arithmétiques

valeur1 + valeur2 : addition : std::ops::Add 
valeur1 - valeur2 : soustraction : std::ops::Sub 
valeur1 * valeur2 : multiplication : std:ops:Mul 
valeur1 / valeur2 : division : std:ops::Div 
valeur1 % valeur2 : reste de la division : std:ops::Rem 

Opérateurs arithmétiques et d’assignation

valeur1 += valeur2 : addition assignée...

Inventaire et usage de quelques traits prédéfinis

1. L’usage de Derive

a. Explications

L’instruction Derive que nous avons déjà utilisée dans cet ouvrage, sans toutefois l’expliciter, permet d’indiquer facilement une liste de traits prédéfinis au compilateur. Ce dernier va pouvoir proposer des implémentations par défaut. En clair, on délègue au compilateur l’écriture des méthodes relatives aux traits indiqués.

Attention, tous les traits prédéfinis, donc issus en général de la librairie standard, ne sont pas éligibles à ce super-pouvoir que constitue le recours en dérive, que nous pouvons résumer ainsi : « Demander au compilateur de gérer les détails quant aux traits que l’on désire voir implémentés par la structure que l’on est en train de concevoir ».

Le lien suivant indique la page de documentation de cette instruction Derive : https://doc.rust-lang.org/rust-by-example/trait/derive.html

Les traits éligibles sont les suivants :

  • Des traits de comparaison : Eq, PartialEq, Ord et PartialOrd.

  • Le trait Clone, qui permet de créer T depuis une référence &T.

  • Le trait de copie, Copy.

  • Le trait Hash, qui permet de produire un hachage depuis &T.

  • Le trait Default, qui permet de créer une instance par défaut du type considéré.

  • Le trait Debug, qui permet de formater une sortie sur {:?}.

b. Exemple support

On crée tout d’abord un projet support :

Rust % cargo new derives --bin 
>     Created binary (application) `derives` package 

On commence par créer une structure qui dérive des traits PartialEq (opérateur de comparaison) et Clone :

#[derive(PartialEq, Clone)] 
struct Exemple { 
    xx: i32, 
    yy: String, 
} 

Utilisons cette structure :

let a = Exemple{xx: 5, yy: "aa".to_string()}; 
let b = Exemple{xx: 5, yy: "bb".to_string()}; 

On crée deux instances, puis on les compare. On bénéficie alors d’une implémentation de la comparaison grâce à Derive (PartialEq) :

if a == b { 
   println!("Les...