Rust : génériques, traits, lifetimes

De Justine's wiki
Aller à la navigation Aller à la recherche

<syntaxhighlight lang='rust'> //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
   }

}


</syntaxhighlight>