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...