Recherche…


Introduction

Les traits sont un moyen de décrire un «contrat» qu'une struct doit implémenter. Les caractères définissent généralement des signatures de méthode mais peuvent également fournir des implémentations basées sur d'autres méthodes du trait, à condition que les limites de trait le permettent.

Pour ceux qui sont familiers avec la programmation orientée objet, les traits peuvent être considérés comme des interfaces avec quelques différences subtiles.

Syntaxe

  • trait Trait {méthode fn (...) -> ReturnType; ...}
  • trait Trait: Bound {fn method (...) -> ReturnType; ...}
  • impliquer Trait for Type {fn method (...) -> ReturnType {...} ...}
  • implicite <T> Trait pour T où T: Bounds {méthode fn (...) -> ReturnType {...} ...}

Remarques

  • Les traits sont généralement assimilés à des interfaces, mais il est important de faire la distinction entre les deux. Dans les langages OO tels que Java, les interfaces font partie intégrante des classes qui les étendent. Dans Rust, le compilateur ne sait rien des traits d'une structure à moins que ces traits ne soient utilisés.

Les bases

Créer un trait

trait Speak {
    fn speak(&self) -> String;
}

Implémenter un trait

struct Person;
struct Dog;

impl Speak for Person {
    fn speak(&self) -> String {
        String::from("Hello.")
    }
}

impl Speak for Dog {
    fn speak(&self) -> String {
        String::from("Woof.")
    }
}

fn main() {
    let person = Person {};
    let dog = Dog {};
    println!("The person says {}", person.speak());
    println!("The dog says {}", dog.speak());
}

Envoi statique et dynamique

Il est possible de créer une fonction qui accepte des objets qui implémentent un trait spécifique.

Envoi statique

fn generic_speak<T: Speak>(speaker: &T) {
    println!("{0}", speaker.speak());
}

fn main() {
    let person = Person {};
    let dog = Dog {};

    generic_speak(&person);
    generic_speak(&dog);
}

La répartition statique est utilisée ici, ce qui signifie que le compilateur Rust générera des versions spécialisées de la fonction generic_speak pour les types Dog et Person . Cette génération de versions spécialisées d'une fonction polymorphe (ou de toute entité polymorphe) au cours de la compilation s'appelle Monomorphization .

Envoi dynamique

fn generic_speak(speaker: &Speak) {
    println!("{0}", speaker.speak());
}

fn main() {
    let person = Person {};
    let dog = Dog {};

    generic_speak(&person as &Speak);
    generic_speak(&dog); // gets automatically coerced to &Speak
}

Ici, une seule version de generic_speak existe dans le fichier binaire compilé, et l'appel speak() est effectué à l'aide d'une recherche vtable à l'exécution. Ainsi, l'utilisation de la distribution dynamique accélère la compilation et réduit la taille des fichiers binaires compilés, tout en étant légèrement plus lente à l'exécution.

Les objets de type &Speak ou Box<Speak> sont appelés objets de trait .

Types associés

  • Utilisez le type associé lorsqu'il existe une relation biunivoque entre le type implémentant le trait et le type associé.
  • Il est parfois également appelé type de sortie , car il s’agit d’un élément donné à un type lorsque nous lui appliquons un trait.

Création

trait GetItems {
    type First;
//  ^~~~ defines an associated type. 
    type Last: ?Sized;
//           ^~~~~~~~ associated types may be constrained by traits as well
    fn first_item(&self) -> &Self::First;
//                           ^~~~~~~~~~~ use `Self::` to refer to the associated type 
    fn last_item(&self) -> &Self::Last;
//                          ^~~~~~~~~~ associated types can be used as function output...
    fn set_first_item(&mut self, item: Self::First);
//                                     ^~~~~~~~~~~  ... input, and anywhere.
}

Mise en œuvre

impl<T, U: ?Sized> GetItems for (T, U) {
    type First = T;
    type Last = U;
//              ^~~ assign the associated types
    fn first_item(&self) -> &Self::First { &self.0 }
    fn last_item(&self) -> &Self::Last { &self.1 }
    fn set_first_item(&mut self, item: Self::First) { self.0 = item; }
}

impl<T> GetItems for [T; 3] {
    type First = T;
    type Last = T;
    fn first_item(&self) -> &T { &self[0] }
//                           ^ you could refer to the actual type instead of `Self::First`
    fn last_item(&self) -> &T { &self[2] }
    fn set_first_item(&mut self, item: T) { self[0] = item; }
}

Référencement aux types associés

Si nous sommes sûrs qu'un type T implémente GetItems par exemple dans les génériques, nous pourrions simplement utiliser T::First pour obtenir le type associé.

fn get_first_and_last<T: GetItems>(obj: &T) -> (&T::First, &T::Last) {
//                                               ^~~~~~~~ refer to an associated type
    (obj.first_item(), obj.last_item())
}

Sinon, vous devez indiquer explicitement au compilateur la caractéristique implémentée par le type

let array: [u32; 3] = [1, 2, 3];
let first: &<[u32; 3] as GetItems>::First = array.first_item();
//          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [u32; 3] may implement multiple traits which many
//                                        of them provide the `First` associated type.
//                                        thus the explicit "cast" is necessary here.
assert_eq!(*first, 1);

Contrainte avec les types associés

fn clone_first_and_last<T: GetItems>(obj: &T) -> (T::First, T::Last)
    where T::First: Clone, T::Last: Clone
//  ^~~~~ use the `where` clause to constraint associated types by traits
{
    (obj.first_item().clone(), obj.last_item().clone())
}

fn get_first_u32<T: GetItems<First=u32>>(obj: &T) -> u32 {
//                          ^~~~~~~~~~~ constraint associated types by equality
    *obj.first_item()
}

Méthodes par défaut

trait Speak {
    fn speak(&self) -> String {
        String::from("Hi.")
    }
}

La méthode sera appelée par défaut sauf si elle est écrasée dans le bloc impl .

struct Human;
struct Cat;

impl Speak for Human {}

impl Speak for Cat {
    fn speak(&self) -> String {
        String::from("Meow.")
    }
}

fn main() {
    let human = Human {};
    let cat = Cat {};
    println!("The human says {}", human.speak());
    println!("The cat says {}", cat.speak());
}

Sortie:

L'humain dit bonjour.

Le chat dit Miaou.

Placer une borne sur un trait

Lors de la définition d'un nouveau trait, il est possible d'imposer aux types souhaitant implémenter ce trait de vérifier un certain nombre de contraintes ou de limites.

Prenant un exemple de la bibliothèque standard, le trait DerefMut exige qu’un type implémente d’abord son trait de caractère Deref :

pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}

Cela permet à DerefMut d'utiliser le type de Target associé défini par Deref .


Bien que la syntaxe puisse rappeler l'héritage:

  • il apporte tous les éléments associés (constantes, types, fonctions, ...) du trait lié
  • il permet le polymorphisme de &DerefMut à &Deref

C'est différent dans la nature:

  • il est possible d'utiliser une durée de vie (telle que 'static )
  • il n'est pas possible de remplacer les éléments de trait liés (même pas les fonctions)

Il est donc préférable de le considérer comme un concept distinct.

Plusieurs types d'objet lié

Il est également possible d'ajouter plusieurs types d'objet à une fonction de distribution statique .

fn mammal_speak<T: Person + Dog>(mammal: &T) {
    println!("{0}", mammal.speak());
}

fn main() {
    let person = Person {};
    let dog = Dog {};

    mammal_speak(&person);
    mammal_speak(&dog);
}


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow