Rust : génériques, traits, lifetimes

De Justine's wiki
Version datée du 3 novembre 2022 à 09:16 par Justine (discussion | contributions) (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. // //... »)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigation Aller à la recherche
//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
    }
}