Génériques et traits Rust
Les génériques sont un mécanisme indispensable dans les langages de programmation.
C++ utilise des "templates" pour implémenter des génériques, tandis que C n'a pas de mécanisme générique, ce qui rend difficile pour C de construire des projets avec des types complexes.
Le mécanisme générique est un mécanisme utilisé par les langages de programmation pour exprimer l'abstraction de type, généralement utilisé pour des classes dont la fonctionnalité est déterminée mais dont les types de données ne sont pas spécifiés, comme les listes chaînées et les tables associatives.
Définir des génériques dans les fonctions
Voici une méthode pour le tri par sélection de nombres entiers :
Exemple
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
fn main() {
let a = [2, 4, 6, 3, 1];
println!("max = {}", max(&a));
}
Sortie :
max = 6
Il s'agit d'un programme simple pour obtenir la valeur maximale, qui peut gérer des données de type numérique i32, mais ne peut pas gérer des données de type f64. En utilisant des génériques, nous pouvons rendre cette fonction utilisable pour divers types. Cependant, en réalité, tous les types de données ne peuvent pas être comparés par taille, donc le prochain extrait de code n'est pas destiné à être exécuté, mais à décrire la syntaxe des génériques de fonction :
Exemple
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
Génériques dans les structures et énumérations
Les énumérations Option et Result que nous avons vues précédemment sont génériques.
Les structures et les énumérations en Rust peuvent toutes deux implémenter le mécanisme générique.
struct Point<T> {
x: T,
y: T
}Ceci est une structure de coordonnées de point, où T représente le type numérique décrivant les coordonnées du point. Nous pouvons l'utiliser comme ceci :
let p1 = Point {x: 1, y: 2};
let p2 = Point {x: 1.0, y: 2.0};Aucun type n'est déclaré lors de son utilisation ; le mécanisme d'inférence de type automatique est utilisé ici, mais les incompatibilités de type ne sont pas autorisées, comme suit :
let p = Point {x: 1, y: 2.0};Lorsque x est lié à 1, T est déjà défini sur i32, donc le type f64 n'est pas autorisé. Si nous voulons que x et y soient représentés par des types de données différents, nous pouvons utiliser deux identifiants génériques :
struct Point<T1, T2> {
x: T1,
y: T2
}Dans les énumérations, les génériques sont exprimés par des constructions telles que Option et Result :
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}Les structures et les énumérations peuvent définir des méthodes, donc les méthodes doivent également implémenter le mécanisme générique ; sinon, les types génériques ne peuvent pas être effectivement manipulés par les méthodes.
Exemple
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("p.x = {}", p.x());
}
Sortie :
p.x = 1
Notez que le mot-clé impl doit être suivi de<T><T>, car le T qui suit est calqué sur lui.
impl Point<f64> {
fn x(&self) -> f64 {
self.x
}
}Les génériques du bloc impl lui-même n'empêchent pas ses méthodes internes d'avoir des capacités génériques :impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}La méthode mixup fusionne le x d'un point Point<T, U> avec le y d'un point Point<V, W> en un nouveau point de type Point<T, W>.
Traits
Le concept de traits est proche des interfaces en Java, mais les deux ne sont pas exactement identiques. Le point commun entre les traits et les interfaces est qu'ils sont tous deux des spécifications comportementales, utilisées pour identifier quelles classes possèdent quelles méthodes.
Les traits sont représentés en Rust à l'aide du mot-clé trait :
trait Descriptive {
fn describe(&self) -> String;
}
Descriptive spécifie que les implémenteurs doivent avoir une méthodedescribe(&self) -> String.
Nous l'utilisons pour implémenter une struct :
Exemple
name: String,
age: u8
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
Le format est :
impl <特性名> for <所实现的类型名>
En Rust, un même type peut implémenter plusieurs traits, et chaque bloc impl ne peut en implémenter qu'un.
Traits par défaut
C'est la différence entre les traits et les interfaces : les interfaces ne peuvent que spécifier des méthodes, mais ne peuvent pas définir de méthodes, tandis que les traits peuvent définir des méthodes comme méthodes par défaut. Comme elles sont « par défaut », les objets peuvent soit redéfinir les méthodes, soit utiliser les méthodes par défaut sans les redéfinir :
Exemple
fn describe(&self) -> String {
String::from("[Object]")
}
}
struct Person {
name: String,
age: u8
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
fn main() {
let cali = Person {
name: String::from("Cali"),
age: 24
};
println!("{}", cali.describe());
}
Sortie :
Cali 24
Si nous supprimons le contenu dans le bloc impl Descriptive for Person, alors la sortie sera :
[Object]
Traits comme paramètres
Dans de nombreux cas, nous devons passer une fonction comme paramètre, comme les fonctions de rappel, la définition d'événements de bouton, etc. En Java, les fonctions doivent être passées sous forme d'instances de classes qui implémentent une interface ; en Rust, cela peut être réalisé en passant des paramètres de trait :
fn output(object: impl Descriptive) {
println!("{}", object.describe());
}
Tout objet implémentant le trait Descriptive peut être passé comme paramètre à cette fonction. La fonction n'a pas besoin de savoir si l'objet passé possède d'autres propriétés ou méthodes ; elle a seulement besoin de savoir qu'il doit avoir les méthodes spécifiées par le trait Descriptive. Bien sûr, cette fonction ne peut pas non plus utiliser d'autres propriétés ou méthodes.
Les paramètres de trait peuvent également être implémentés avec cette syntaxe équivalente :
fn output<T: Descriptive>(object: T) {
println!("{}", object.describe());
}Il s'agit de sucre syntaxique avec un style similaire aux génériques. Ce sucre syntaxique est très utile lorsque plusieurs types de paramètres sont tous des traits :
fn output_two<T: Descriptive>(arg1: T, arg2: T) {
println!("{}", arg1.describe());
println!("{}", arg2.describe());
}
Lorsque des traits sont utilisés comme représentations de type et que plusieurs traits sont impliqués, vous pouvez utiliser++ comme symbole pour représenter, par exemple :
fn notify(item: impl Summary + Display) fn notify<T: Summary + Display>(item: T)
Remarque :Il n'est utilisé que pour représenter des types ; cela ne signifie pas qu'il peut être utilisé dans des blocs impl.
Les relations d'implémentation complexes peuvent être simplifiées à l'aide du mot-clé where, par exemple :
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
peut être simplifié en :
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
Après avoir compris cette syntaxe, l'exemple « obtenir la valeur maximale » du chapitre sur les génériques peut véritablement être implémenté :
Exemple
fn compare(&self, object: &Self) -> i8;
}
fn max<T: Comparable>(array: &[T]) -> &T {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i].compare(&array[max_index]) > 0 {
max_index = i;
}
i += 1;
}
&array[max_index]
}
impl Comparable for f64 {
fn compare(&self, object: &f64) -> i8 {
if &self > &object { 1 }
else if &self == &object { 0 }
else { -1 }
}
}
fn main() {
let arr = [1.0, 3.0, 5.0, 4.0, 2.0];
println!("maximum of arr is {}", max(&arr));
}
Sortie :
maximum of arr is 5
Astuce :Comme il est nécessaire de déclarer que le deuxième paramètre de la fonction de comparaison doit être identique au type qui implémente le trait, le mot-clé Self (notez la casse) représente le type courant lui-même (et non l'instance).
Les traits comme valeurs de retour
Le format pour les traits en tant que valeurs de retour est le suivant :
Exemple
Person {
name: String::from("Cali"),
age: 24
}
}
Mais il y a un point : lorsqu'un trait est utilisé comme valeur de retour, seuls les objets implémentant le trait sont acceptés comme valeurs de retour, et tous les types de retour possibles dans la même fonction doivent être exactement identiques. Par exemple, si struct A et struct B implémentent tous deux le trait Trait, la fonction suivante est erronée :
Exemple
if bl {
return A {};
} else {
return B {};
}
}
Implémentation conditionnelle de méthodes
La fonctionnalité impl est très puissante ; nous pouvons l'utiliser pour implémenter des méthodes pour un type. Mais pour les types génériques, nous devons parfois distinguer quelles méthodes le type générique auquel il appartient a déjà implémentées afin de décider quelles méthodes il doit implémenter ensuite :
struct A<T> {}
impl<T: B + C> A<T> {
fn d(&self) {}
}Ce code déclare que le type A<T> ne peut effectivement implémenter ce bloc impl que si T a déjà implémenté les traits B et C.
Autres extensions