//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
}
}