En résumé, la méthode du trait doit prendre en paramètre un
&Fn(X) -> Y
et non un
Fn(X) -> Y
Sinon, Rust ne peut pas garantir l'exécution de la fonction puisque le trait prendrait possession de la fonction. Or cette closure peut être un pointeur sur fonction, ce qui retirerait la propriété de cette dernière de sa structure.
Je reprends l'exemple que j'avais donné ici et que j'ai modifié pour le "Yegorifier".
#![allow(non_snake_case)]
// Interface / Trait
trait Addition {
fn add(&self) -> i32;
}
// Couple
struct Couple {
opA: i32,
opB: i32,
}
impl Couple {
// Équivalent à une méthode 'static' de Java simulant un constructeur
fn new(opA: i32, opB: i32) -> Self {
return Couple { opA: opA, opB: opB }
}
}
// Triplet
struct Triplet {
opA: Box<dyn Addition>, // Un trait doit être passé dans un objet de type Box
opB: i32,
}
impl Triplet {
// Équivalent à une méthode 'static' de Java simulant un constructeur
fn new(opA: Box<dyn Addition>, opB: i32) -> Self {
return Triplet { opA: opA, opB: opB }
}
}
// Implémentations du trait "Addition"
impl Addition for Couple {
fn add(&self) -> i32 {
return self.opA + self.opB;
}
}
impl Addition for Triplet {
fn add(&self) -> i32 {
return (*self.opA).add() + self.opB;
}
}
// Exécution
fn main() {
let additionA = Couple::new(1, 2);
println!("Couple add : [{}]", additionA.add()); // Print 3
// Ne pas oublier de wrapper Couple dans une Box
let additionB = Triplet::new(Box::new(additionA), 3);
println!("Triplet add : [{}]", additionB.add()); // Print 6
}
Note personnelle : je n'aime vraiment pas du tout la syntaxe de Rust, mais vraiment :(. À chaque fois que j'en fais je meurs un petit peu. Après je maintiens qu'il s'agit d'un des meilleurs langages du marché à cette heure et une tendance de fond qui me pousse à prendre la vague.
Bon je vais reprendre le début du kata @Kysofer (qui va me faire intervenir dans ses formations) pour montrer comment il programme en OOP "pure" avec Rust en mettant en place une encapsulation stricte (à la Yegor Bugayenko). Le poste se fera en plusieurs parties, cette première étape consistant à montrer comment fonctionnent les trait
, les struct
et la notion de value borrowed
pour les débutants.
Objectifs du kata :
- Fournir deux structures différentes permettant l'addition.
- Décorer la première structure par la seconde.
- Introduction succincte au
value borrowed
.
Code :
#![allow(non_snake_case)] // Oui je viens du monde Java/Kotlin
// Structures
struct Couple {
opA: i32,
opB: i32,
}
struct Triplet {
opA: Couple,
opB: i32,
}
// Interface
trait Addition {
fn add(&self) -> i32;
}
// Implementations
impl Addition for Couple {
fn add(&self) -> i32 {
return self.opA + self.opB;
}
}
impl Addition for Triplet {
fn add(&self) -> i32 {
return self.opA.add() + self.opB;
}
}
Exécution :
Pour instancier et exécuter le code il faut écrire ceci. Tout marche, aucun problème.
fn main() {
let additionA = Couple { opA: 1, opB: 2 };
println!("Couple add : [{}]", additionA.add()); // Print 3
let additionB = Triplet {
opA: additionA,
opB: 3,
};
println!("Triplet add : [{}]", additionB.add()); // Print 6
}
La petite difficulté (el famoso "value borrowed") arrive dès que l'on initialise les deux structures l'une derrière l'autre :
fn main() {
let additionA = Couple { opA: 1, opB: 2 };
let additionB = Triplet {
opA: additionA,
opB: 3,
};
println!("Couple add : [{}]", additionA.add()); // NE COMPILE PAS
println!("Triplet add : [{}]", additionB.add());
}
En réalité, au moment du premier println!()
, la variable additionA
n'est plus dans l'espace courant mais a été empruntée par Triplet
, elle se retrouve donc utilisable mais uniquement par lui. Alors soit on réordonnance le code (ce que j'ai fait dans la première exécution qui fonctionne) soit on indique que Triplet
restitue la variable qu'il a emprunté (je montrerai plus tard comment faire).
Bref, jusque là rien de transcendant nous remarquerons quand même que Triplet
contient un Couple
(c'est-à-dire une structure) et non une Addition
(c'est-à-dire une interface) ce qui matérialise un couplage fort entre les types et donc l'impossibilité de programmer par contrat (c'est-à-dire en masquant les implémentations et en protégeant l'encapsulation des données contenues dans la structure Couple
).
C'est là qu'arrivera la second difficulté dont je parlerai aussi plus tard : comment calculer dynamiquement les tailles des implémentations des traits pour les utiliser en tant qu'attribut d'une structure.
Avis personnel : depuis quelques mois que je m'amuse avec Rust je peux dire que globalement j'aime ce langage mais cela ne m'empêche pas de le trouver pauvre sur pas mal de ses aspects (au sens paradigme de programmation).
Dans ce qui me déplaît, je mettrais la forte emprunte qu'il retire C et du C++, notamment leur style procédurale omniprésent (on ne code plus comme en Pascal en 2020, damned), l'encapsulation découragée totalement (en ce sens Rust n'est pas objet pour un sous, et la programmation en structure-first est un anti-concept, encore une fois cf. Elegant Objects de Yegor Bugayenko) et pour ma plus grande tragédie, une syntaxe que je trouve lourdingue car s'appuyant sur beaucoup de symboles ou trop peux expressive (je suis navrée pour ceux qui pensent que i32 et u32 sont de bonnes façons d'écrire des entiers signés ou non en 2020).
Encore une fois, la syntaxe de Kotlin (sauf le sucre syntaxique du préfixe get/set et les equals methods), l'API immutable de Kotlin, la gestion des Threads/KoRoutines de Kotlin, le null-check de Kotlin, le tout mêlé au Owernship de Rust, à la performance du binaire produit par son compilateur, à sa capacité à gérer du bas niveau, tout ceci en ferait sûrement l'un des langages les plus efficaces et fiable du siècle.
Je prie pour que Kotlin Native soit un "game changer" notamment pour qu'il parvienne à s'élever au niveau de Rust en terme de performances et de protection contre les race-conditions.