Rust : génériques, traits, lifetimes
<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>