Ricerca…


introduzione

I tratti sono un modo per descrivere un "contratto" che una struct deve implementare. I tratti in genere definiscono le firme dei metodi, ma possono anche fornire implementazioni basate su altri metodi del tratto, a condizione che i limiti dei tratti lo consentano.

Per chi ha familiarità con la programmazione orientata agli oggetti, i tratti possono essere pensati come interfacce con alcune sottili differenze.

Sintassi

  • tratto Trait {metodo fn (...) -> ReturnType; ...}
  • tratto Trait: Bound {fn method (...) -> ReturnType; ...}
  • impl Trait for Type {fn method (...) -> ReturnType {...} ...}
  • impl <T> Trait for T where T: Bounds {fn method (...) -> ReturnType {...} ...}

Osservazioni

  • I tratti sono comunemente paragonati alle interfacce, ma è importante fare una distinzione tra i due. Nei linguaggi OO come Java, le interfacce sono parte integrante delle classi che li estendono. In Rust, il compilatore non sa nulla dei tratti di una struttura a meno che questi tratti non vengano utilizzati.

Nozioni di base

Creare un tratto

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

Implementazione di un tratto

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());
}

Invio statico e dinamico

È possibile creare una funzione che accetta oggetti che implementano un tratto specifico.

Spedizione statica

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);
}

In questo caso viene utilizzato il dispatch statico, il che significa che il compilatore di Rust genererà versioni specializzate della funzione generic_speak per i tipi Dog e Person . Questa generazione di versioni specializzate di una funzione polimorfica (o di qualsiasi entità polimorfica) durante la compilazione si chiama monomorfizzazione .

Invio dinamico

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
}

Qui, esiste solo una singola versione di generic_speak nel binario compilato e la chiamata speak() viene effettuata utilizzando una ricerca vtable in fase di runtime. Pertanto, l'utilizzo di risultati di invio dinamico consente una compilazione più rapida e dimensioni più ridotte del binario compilato, pur essendo leggermente più lento in fase di runtime.

Oggetti di tipo &Speak o Box<Speak> sono chiamati oggetti tratto .

Tipi associati

  • Utilizzare il tipo associato quando esiste una relazione uno-a-uno tra il tipo che implementa la caratteristica e il tipo associato.
  • Talvolta è anche noto come tipo di output , poiché si tratta di un elemento assegnato a un tipo quando applichiamo un tratto.

Creazione

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

L'implementazione

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

Riferendosi ai tipi associati

Se siamo sicuri che un tipo T implementa GetItems ad esempio in generici, potremmo semplicemente usare T::First per ottenere il tipo associato.

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

Altrimenti, devi dire esplicitamente al compilatore quale caratteristica sta implementando il tipo

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);

Limitazione con tipi associati

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()
}

Metodi predefiniti

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

Il metodo verrà chiamato di default eccetto se è sovrascritto nel blocco 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());
}

Produzione :

L'umano dice Ciao.

Il gatto dice Meow.

Posizionare un limite su un tratto

Quando si definisce una nuova caratteristica, è possibile applicare i tipi che desiderano implementare questa caratteristica per verificare una serie di vincoli o limiti.

Prendendo un esempio dalla libreria standard, il tratto DerefMut richiede che un tipo implementi innanzitutto il suo tratto di Deref :

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

Questo, a sua volta, consente a DerefMut di utilizzare il tipo associato Target definito da Deref .


Mentre la sintassi potrebbe ricordare l'ereditarietà:

  • porta in tutti gli elementi associati (costanti, tipi, funzioni, ...) del tratto associato
  • abilita il polimorfismo da &DerefMut a &Deref

Questo è diverso in natura:

  • è possibile utilizzare una durata (come 'static ) come limite
  • non è possibile sovrascrivere gli elementi tratti associati (nemmeno le funzioni)

Quindi è meglio pensarlo come un concetto separato.

Più tipi di oggetti associati

È anche possibile aggiungere più tipi di oggetto a una funzione di rilascio statico .

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
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow