« Rust : génériques, traits, lifetimes » : différence entre les versions
Aller à la navigation
Aller à la recherche
Page créée avec « <nowiki> //1 - Generic Data Types //Le but principal est de diminuer la duplication de code. //Les génériques sont utilisés pour créer par exemple des signatures de //fonction ou des structs. //Dans une fonction //Par exemple ici, je veux extraire le plus gros chiffe d'une liste de i32 //et le plus gros char d'une liste de chars (pas un String); je pourrais //faire 2 fonctions, ou une seule avec un générique. // //... » |
(Aucune différence)
|
Version du 3 novembre 2022 à 09:16
//1 - Generic Data Types //Le but principal est de diminuer la duplication de code. //Les génériques sont utilisés pour créer par exemple des signatures de //fonction ou des structs. //Dans une fonction //Par exemple ici, je veux extraire le plus gros chiffe d'une liste de i32 //et le plus gros char d'une liste de chars (pas un String); je pourrais //faire 2 fonctions, ou une seule avec un générique. // //Je commence par noter mon générique (qui est en gros un placeholder // pour le type) entre chevrons. Souvent noté T par convention. // //La signature suivante signifie: //"La fonction largest est générique sur un type noté T. Elle prend //en paramètre une ref vers une liste d'objets de type T, et renvoie //une ref vers un objet de type T". fn largest<T>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } //Ici, T n'est pas restreint : il peut prendre n'importe quel type. //D'ailleurs, ça compile pas : l'opérateur > ne fonctionne //pas avec tous les types, alors le compilateur il gueule. //Dans un struct //On peut aussi définir un struct en utilisant un générique. struct Point<T> { x: T, y:T } fn main() { let integer = Point { x:5, y:10 }; let float = Point { x:1.0,, y:4.0 }; //Ici, on a que T donc les 2 valeurs doivent être du même type //let marchepas = Point {x:5, y:4.2}; } //Dans ce genre de cas on peut utiliser 2 génériques struct Point2<T, U> { x: T, y: U } //Appellable avec let bidule = Point {x: 4, y: 5.2 }; //Dans un enum //Option<T> le fait déjà: //enum Option<T> { // Some(T), // None, //} // //Result a 2 génériques //enum Result<T, E> { // Ok(T), // Err(E) //} //Dans une méthode //Même idée : le générique utilisé pour mon struct peut être //réutilisé dans les méthodes du Struct struct Point2<T> { x: T, y: T } impl<T> Point2<T> { fn x(&self) -> &T { &self.x } } //Cependant on a une autre possibilité : //Réserver une méthode au cas où T est d'un type précis, //par exemple f32: impl Point2<f32>{ fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } //Ici, un Point2<f32> aura la méthode distance_from_origin; //un Point<i32> ne l'aura pas. //Enfin, les méthodes peuvent avoir des génériques en plus. //Exemple : struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); } //Les génériques n'ont pas spécialement de coût en performances. // ---------------------------------------------------------------------- // Traits : Defining Shared Behavior //Un trait définit une fonctionnalité qu'un type a et peut partager avec //d'autres types; on parle de shared behaviour. //On peut utiliser un "trait bound" pour spếcifier //qu'un générique peut être de n'importe quel type ayant tel ou tel trait. //Definir un trait //L'attitude (behaviour) d'un type, c'est ses méthodes. Elles sont //partagées si plusieurs types ont la même. Les traits sont une façon //de grouper des signatures de méthodes pour définir des ensembles //d'attitudes. //Exemple, on veut grouper des articles de presse et des tweets. //Chacun à un struct, et tous ont une méthode qui renvoie un résumé; //cependant un tweet n'est pas fait comme un article de presse ! pub trait Summary { fn summarize(&self) -> String; } //On a juste la signature; chaque type ayant ce trait devra fournir sa //propre implémentation. Le compilateur s'attendra à ce que chaque type //ayant le trait Summary ait une méthode summarize. //Un trait peut avoir plusieurs méthodes. //Implémenter le trait pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } //C'est comme ça qu'on donne le trait Summarize a NewsArticle impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } //On pourrait par la suite appeller le type tweet et son trait avec ceci, //en admettant que j'ai mis ça dans une lib nommée aggregator //use aggregator::{Summary, Tweet} // //À noter : on ne peut implémenter un trait sur un type que si le trait //ou (OR) le type est local //Avoir une méthode par défaut //On peut avoir une méthode par défaut associée à chaque signature de fonction //donnée dans notre trait; on pourra la conserver ou l'override lors de //l'implémentation. pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } //Pour avoir la méthode par défaut, on DOIT donner une implémentation vide. impl Summary for NewsArticle {} //Une implémentation par défaut peut appeller d'autre méthodes du même trait, //même si ces dernières ne sont pas dans l'implémentation par défaut. pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } //Ici, mon implémentation n'as besoin de définir que summarize_author impl Summary for Tweet { fn summarize_author(&self) -> String { format!("@{}", self.username) } } //Traits comme paramètre //Du coup, on peut créer une fonction prenant pour paramètre un objet //dont on précise non pas son type, mais un trait qu'il doit avoir. pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } //Ici, je me fiche de savoir quel est le type de "item", simplement, qu'il //doit avoir le trait Summary. //Trait Bound //La syntaxe ci-dessus fonctionne dans les cas simple, mais en réalité //c'est juste du sucre syntaxique pour la forme suivante nommée trait bound: pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize()); } //La forme "&impl Trait" est pratique et permet de raccourcir. //On peut avoir 2 paramètres qui doivent avoir un trait peu importe //leur type, par ex: pub fn notify(item1: &impl Summary, item2: &impl Summary) { println!("Whatever"); } //...mais si ils doivent avoir le même type il nous faut un trait bound (super) pub fn notify<T: Summary>(item1: &T, item2: &T) { println!("Whatever"); } //On peut aussi avoir un bound sur 2 traits à la fois. pub fn notify(item: &(impl Summary + Display)) { println!("Whatever"); } //Ça marche aussi en trait bound pub fn notify<T: Summary + Display>(item: &T) { println!("Whatever"); } //On a un syntaxe alternative si on a beaucoup de trait bound, pour rendre //ça plus lisible //Par exemple transformer ça: fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { //[...] } //...en ça avec le mot-clef where fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug, { //[...] } //Quel foutoir ! //Trait bound, mais sur le retour de la fonction //On peut appliquer le principe du trait bound à la valeur de retour d'une //fonction: fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } //Ici, ma valeur de retour peut avoir n'importe quel type ayant le trait //Summary. Mais en fait non : si ma fonction peut renvoyer plusieurs types //différents (par exemple un tweet ou un NewsArticle en fonction des cas), //le compilateur plantera. Zut ! //Trait bounds pour implémentation conditionnelles de méthodes //Alors, en plus on peut faire des trait bounds sur des implémentations //pour dire qu'un objet aura telle méthode seulement si il a tel trait: use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } //Ici, un objet de type Pair aura toujours la méthode new, sa methode getter; //Mais il n'aura la méthode cmp.display QUE si il a le trait Display ET le trait //PartialOrd. //On peut aussi implémenter un trait pour n'importe quel type qui implémente //un autre trait; c'est très utilisé dans la lib standard, par exemple pour //ToString qui est implémenté seulement si le type a le trait Display. impl<T: Display> ToString for T { // --snip-- } //Cela s'appelle une "blanket implementation". //------------------------------------------------------- // // Lifetimes : Validating References // // Les lifetimes sont aussi des génériques. Ils servent à s'assurer qu'une // référence est valide tant qu'on en a besoin. // // Toute &référence a un lifetime, soit un scope dans lequel elle est valide. // Elles sont la plupart du temps implicite, comme les types. // Et comme les types, il faut les préciser quand ce n'est pas déductible. // C'est une notion unique à Rust. //EXPLICATIONS PREALABLES //Le but des lifetime est d'éviter les références "dangling", qui ne mène //plus nulle part. //Exemple: fn main() { //On peut faire ce genre d'assignation sans valeur //mais le compilateur plantera si on ne donne pas une valeur à r //avant de l'utiliser (no null values in Rust). //À eviter, je suppose. let r; { let x = 5; r = &x; } println!("r: {}", r); } //r mène à x, mais x n'existe plus lors du println!. //Rust le détermine à l'aide de son "Borrow Checker"; celui-ci vérifie //les emprunts. Même code qu'au dessus, mais on montre les lifetimes. //notés 'a et 'b. fn main() { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } //APPLICATION //Prenons le code suivant, qui compare des string slices (qui sont par nature //des références), et qui ne compilera pas : fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } //Ici le compilateur nous donnera une erreur pour réclamer un lifetime parameter //pour le retour. //En effet, on ne sait pas à quoi la référence renvoyée fera référence à cause //du if ! // On va ajouter des generic lifetime parameters. //Les annotation de lifetime ne changent pas la durée de vie des références. //Elles servent juste à décrire les relation des lifetimes et n'ont pas de sens //si je n'en ai qu'une. //Une fonction peut accepter un lifetime générique de la même façon //qu'elle peut accepter un type générique. //Elles sont notées par un ' et s'appellent généralement 'a, puis 'b, 'c etc. //Elles se placent après le & de la référence avec un espace. Par ex: &i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime //entre elles. //Alors reprenons notre fonction et donnons lui des annotations de lifetime. //On veut dire à Rust : la valeur de retour est valide tant que les 2 paramètres //sont valides. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } //On note le <'a> sur le même principe qu'un type générique <T>. //Les annotations ne vont que dans la signature de la fonction. //Ici, lors de l'appel, le lifetime concret de la valeur renvoyée sera //substituée par 'a, qui correspond au scope où se trouveront à la fois x et y, //soit le plus petit des 2 lifetimes (lifetime de x et lifetime de y). //Ceci fonctionne: fn main() { let string1 = String::from("long string is long"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {}", result); } } //Ceci ne fonctionne pas car lors du println!, string2 n'existe plus. //result ayant le lifetime du plus petit des 2 paramètres, lui non plus n'est //plus valide. Cela même si String2 n'est pas la valeur choisie par la fonction. fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result); } //En revanche, ceci fonctionnerait parce que le lifetime de y //n'as pas de rapport avec le lifetime du renvoi fn longest<'a>(x: &'a str, y: &str) -> &'a str { x } //Le même principe peut s'appliquer sur un struct si celui-ci contient une //référence. //Ici, le lifetime du bras part est relié à celui de la valeur du string //à laquelle la slice mise dans part fait référence. struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; } //Ici, j'ai rien compris sur les règles d'elision et les méthodes //Static lifetime //Il existe un lifetime special : 'static //Il signifie que la référence peut vivre toute la durée du programme. //Elle s'applique par défaut aux string litterals. let s: &'static str = "I have a static lifetime."; //Le texte est stocké en dur dans le binaire, qui est toujours accessible. //Il est très rare qu'utiliser ce lifetime ailleurs soit une bonne idée. //LES 3 ENSEMBLE fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {}", result); } use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {}", ann); if x.len() > y.len() { x } else { y } }