English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Les generics sont un mécanisme indispensable pour un langage de programmation.
C++ Les generics sont réalisés par des "modèles" dans le langage, tandis que le langage C n'a pas de mécanisme de generics, ce qui rend difficile la construction de projets de type complexe en langage C.
Le mécanisme de generics est utilisé par les langages de programmation pour exprimer l'abstraction de type, généralement utilisé pour les classes dont les fonctionnalités sont déterminées et les types de données sont indéterminés, comme les listes chainées, les tableaux de hachage, etc.
C'est une méthode de tri par sélection pour les nombres entiers :
fn max(array: &[i32]) -> i32 {}} let mut max_index = 0; let mut i = 1; while i < array.len() { si 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)); }
Résultat de l'exécution :
max = 6
C'est un programme simple pour trouver la valeur maximale, qui peut être utilisé pour traiter i32 Les données de type numérique, mais ne peuvent pas être utilisées pour f64 Les données de type. En utilisant les generics, nous pouvons faire en sorte que cette fonction puisse être utilisée avec tous les types. Mais en réalité, pas tous les types de données peuvent être comparés, donc le prochain morceau de code n'est pas utilisé pour exécuter, mais pour décrire la syntaxe de la generics du fonction :
fn max<T>(array: &[T]) -> T { let mut max_index = 0; let mut i = 1; while i < array.len() { si array[i] > array[max_index] { max_index = i; } i += 1; } array[max_index] }
Dans les classes d'ensembles Option et Result que nous avons appris précédemment, c'est générique .
Les structures et les classes d'ensembles en Rust peuvent tous implémenter le mécanisme générique .
struct Point<T> { x : T , y : T }
C'est une structure de coordonnées de point, T représente le type numérique de description des coordonnées du point. Nous pouvons l'utiliser ainsi :
let p1 = Point { x : 1, y : 2}; let p2 = Point { x : 1.0, y : 2.0};
Lors de l'utilisation, le type n'est pas déclaré, ici l'utilisation est le mécanisme de type automatique, mais il n'est pas permis d'avoir des types non correspondants comme suit :
let p = Point { x : 1, y : 2.0};
x et 1 lors du liage, T a déjà été défini sur i32donc il n'est pas permis de voir f64 de type . Si nous voulons que x et y soient représentés par des types de données différents, nous pouvons utiliser deux identificateurs génériques :
struct Point<T1, T2> { x : T1, y : T2 }
Les méthodes génériques telles que Option et Result dans les classes d'ensembles :
enum Option<T> { Some(T), None , } enum Result<T, E> { Ok(T), Err(E), }
Les structures et les classes d'ensembles peuvent définir des méthodes, alors les méthodes doivent également implémenter le mécanisme générique, sinon les classes génériques ne peuvent pas être opérées efficacement par des méthodes.
struct Point<T> { 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()); }
Résultat de l'exécution :
p.x = 1
Attention, il doit y avoir <T> après le mot-clé impl, car T suivant est utilisé comme modèle. Mais nous pouvons également ajouter une méthode à l'un des génériques :
impl Point<f64> { fn x(&self) -> f64 {}} self.x } }
impl Le bloc générique en lui-même ne freine pas la capacité générique des méthodes internes :
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 les coordonnées x d'un Point<T, U> avec les coordonnées y d'un Point<V, W> pour former un nouveau point de type Point<T, W>.
Le concept de trait est proche de celui de l'interface (Interface) en Java, mais ils ne sont pas complètement identiques. Les traits, comme les interfaces, sont une norme de comportement qui peut être utilisée pour identifier quels types ont quelles méthodes.
Les traits sont représentés par trait en Rust :
trait Descriptive { fn describe(&self) -> String; }
Descriptive spécifie que l'implélateur doit avoir est describe(&self) -> Méthode String.
Nous l'utilisons pour implémenter une structure :
struct Person { name: String, age: u8 } impl Descriptive for Person { fn describe(&self) -> String { format!("{} {}", self.name, self.age) } }
Le format est :
impl <nom de trait> for <nom du type implémenté>
En Rust, une même classe peut implémenter plusieurs traits, chaque bloc impl ne peut implémenter qu'un seul.
C'est la différence entre les traits et les interfaces : les interfaces ne peuvent normer les méthodes mais ne peuvent pas définir de méthodes, mais les traits peuvent définir des méthodes comme des méthodes par défaut, car elles sont "par défaut", donc l'objet peut redéfinir la méthode ou utiliser la méthode par défaut sans la redéfinir :
trait Descriptive { 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()); }
Résultat de l'exécution :
Cali 24
Si nous supprimons le contenu du bloc impl Descriptive for Person, le résultat de l'exécution sera :
[Object]
Dans de nombreux cas, nous devons passer une fonction en tant que paramètre, par exemple des fonctions de rappel, des événements de bouton de configuration, etc. En Java, la fonction doit être passée en exemple de classe implémentant l'interface, en Rust, cela peut être réalisé en passant des paramètres de caractéristiques :
fn output(object: impl Descriptive) { println!("{}", object.describe()); }
Toute instance qui a implémenté la caractéristique Descriptive peut être utilisée comme paramètre de cette fonction. Cette fonction n'a pas besoin de savoir si l'objet传入 a d'autres attributs ou méthodes, il suffit de savoir qu'il a la méthode spécifiée par la norme Descriptive. Bien sûr, d'autres attributs et méthodes ne peuvent pas être utilisés à l'intérieur de cette fonction.
Les paramètres caractéristiques peuvent également être réalisés par cette grammar équivalente: }}
fn output<T: Descriptive>(object: T) { println!("{}", object.describe()); }
C'est un grammar du style générique, très pratique quand plusieurs paramètres de type sont caractéristiques:
fn output_two<T: Descriptive>(arg1: T, arg2: T) { println!("{}", arg1).describe()); println!("{}", arg2).describe()); }
Lorsque le caractéristique est représenté par type, si il est impliqué par plusieurs caractéristiques, on peut utiliser + représentation symbolique, par exemple:
fn notify(item: impl Summary + Display) fn notify<T: Summary + Display>(item: T)
Attention: Neutre à l'expression du type, ceci ne signifie pas que vous pouvez l'utiliser dans le bloc impl.
Les relations d'implémentation complexes peuvent être simplifiées par le 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
Dans l'après compréhension de ce grammar, le cas "obtenir le maximum" du chapitre générique peut vraiment être mis en œuvre :
trait Comparable { 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 de arr est {}", max(&arr)); }
Résultat de l'exécution :
maximum de arr est 5
Conseil : Puisque la déclaration de la fonction compare nécessite que le second paramètre soit du même type que le type implémentant le trait, donc le mot-clé Self (attention à la casse) représente le type actuel (pas l'exemple) lui-même.
Format de retour de valeur de trait :
fn person() -> impl Descriptive { Person { name: String::from("Cali"), age: 24 } }
Mais il y a un point, les traits en tant que valeurs de retour acceptent uniquement les objets qui ont implémenté le trait et tous les types possibles de retour dans la même fonction doivent être complètement identiques. Par exemple, les structures A et B ont toutes deux implémenté le trait Trait, le fonction suivant est incorrect :
fn some_function(bool bl) -> impl Descriptive { if bl { return A {}; } else { return B {}; } }
Les fonctionnalités impl sont très puissantes, nous pouvons les utiliser pour implémenter les méthodes des classes. Cependant, pour les classes génériques, parfois nous devons distinguer les méthodes implémentées par le générique pour décider de la méthode suivante à implémenter :
struct A<T> {} impl<T: B + C> A<T> { fn d(&self) {} }
Cette ligne de code déclare que le type A<T> doit être implémenté de manière effective sous réserve que T ait déjà implémenté les traits B et C.