Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Definire il Comportamento Condiviso con i Trait

Un tratto (trait) definisce la funzionalità che un particolare type ha e può condividere con altri type. Possiamo usare i trait per definire il comportamento condiviso in modo astratto. Possiamo usare i vincoli del tratto (trait bound) per specificare che un type generico può essere qualsiasi type che abbia un determinato comportamento.

Nota: i trait sono simili a una funzionalità spesso chiamata interfacce (interfaces) in altri linguaggi, sebbene con alcune differenze.

Definire un Trait

Il comportamento di un type consiste nei metodi che possiamo chiamare su quel type. Type diversi condividono lo stesso comportamento se possiamo chiamare gli stessi metodi su tutti quei type. Le definizioni dei trait sono un modo per raggruppare le firme dei metodi per definire un insieme di comportamenti necessari per raggiungere un determinato scopo.

Ad esempio, supponiamo di avere più struct che contengono vari tipi e quantità di testo: una struttura Articolo che contiene una notizia archiviata in una posizione specifica e una PostSocial che può contenere, al massimo, 280 caratteri insieme a metadati che indicano se si tratta di un nuovo post, una ripubblicazione o una risposta a un altro post.

Vogliamo creare una libreria di aggregazione multimediale denominata aggregatore in grado di visualizzare riepiloghi dei dati che potrebbero essere memorizzati in un’istanza di Articolo o PostSocial. Per fare ciò, abbiamo bisogno di un riepilogo per ciascun type e richiederemo tale riepilogo chiamando un metodo riassunto su un’istanza. Il Listato 10-12 mostra la definizione di un trait pubblico Sommario che esprime questo comportamento.

File: src/lib.rs
pub trait Sommario {
    fn riassunto(&self) -> String;
}
Listato 10-12: Un trait Sommario che consiste nel comportamento fornito da un metodo riassunto

Qui, dichiariamo un trait usando la parola chiave trait e poi il nome del trait, che in questo caso è Sommario. Dichiariamo anche il trait come pub in modo che anche i crate che dipendono da questo crate possano utilizzare questo trait, come vedremo in alcuni esempi. All’interno delle parentesi graffe, dichiariamo le firme dei metodi che descrivono i comportamenti dei type che implementano questo trait, che in questo caso è fn riassunto(&self) -> String.

Dopo la firma del metodo, invece di fornire un’implementazione tra parentesi graffe, utilizziamo un punto e virgola. Ogni type che implementa questo trait deve fornire il proprio comportamento personalizzato per il corpo del metodo. Il compilatore imporrà che qualsiasi type che abbia il trait Sommario abbia il metodo riassunto definito esattamente con questa firma.

Una trait può avere più metodi nel suo corpo: le firme dei metodi sono elencate una per riga e ogni riga termina con un punto e virgola.

Implementare un Trait su un Type

Ora che abbiamo definito le firme desiderate dei metodi del trait Sommario, possiamo implementarlo sui type nel nostro aggregatore multimediale. Il Listato 10-13 mostra un’implementazione del trait Sommario sulla struct Articolo che utilizza il titolo, l’autore e la posizione per creare il valore di ritorno di riassunto. Per la struct PostSocial, definiamo riassunto come il nome utente seguito dall’intero testo del post, supponendo che il contenuto del post sia già limitato a 280 caratteri.

File: src/lib.rs
pub trait Sommario {
    fn riassunto(&self) -> String;
}

pub struct Articolo {
    pub titolo: String,
    pub posizione: String,
    pub autore: String,
    pub contenuto: String,
}

impl Sommario for Articolo {
    fn riassunto(&self) -> String {
        format!("{}, di {} ({})", self.titolo, self.autore, self.posizione)
    }
}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto(&self) -> String {
        format!("{}: {}", self.nomeutente, self.contenuto)
    }
}
Listato 10-13: Implementazione del trait Sommario sui type Articolo e PostSocial

Implementare un trait su un type è simile a come normalmente sono implementati i metodi. La differenza è che dopo impl, inseriamo il nome del trait che vogliamo implementare, poi utilizziamo la parola chiave for e infine specifichiamo il nome del type per cui vogliamo implementare il trait. All’interno del blocco impl, inseriamo le firme dei metodi definite dalla definizione del trait. Invece di aggiungere un punto e virgola dopo ogni firma, utilizziamo le parentesi graffe e riempiamo il corpo del metodo con il comportamento specifico che vogliamo che i metodi del trait abbiano per quel particolare type.

Ora che la libreria ha implementato il trait Sommario su Articolo e PostSocial, gli utenti del crate possono chiamare i metodi del trait sulle istanze di Articolo e PostSocial nello stesso modo in cui chiamiamo i metodi normali. L’unica differenza è che l’utente deve includere il trait nello scope oltre ai type. Ecco un esempio di come un crate binario potrebbe utilizzare il nostro crate libreria aggregatore:

use aggregatore::{PostSocial, Sommario};

fn main() {
    let post = PostSocial {
        nomeutente: String::from("horse_ebooks"),
        contenuto: String::from(
            "ovviamente, come probabilmente già sapete, gente",
        ),
        risposta: false,
        repost: false,
    };

    println!("1 nuovo post: {}", post.riassunto());
}

Questo codice stampa 1 nuovo post: horse_ebooks: ovviamente, come probabilmente già sapete, gente.

Anche altri crate che dipendono dal crate aggregatore possono includere il trait Sommario nello scope per implementare Sommario sui propri type. Una restrizione da notare è che possiamo implementare un trait su un type solo se il trait o il type, o entrambi, sono locali al nostro crate. Ad esempio, possiamo implementare trait della libreria standard come Display su un type personalizzato come PostSocial come parte della funzionalità del nostro crate aggregatore, perché il type PostSocial è locale al nostro crate aggregatore. Possiamo anche implementare Sommario su Vec<T> nel nostro crate aggregatore, perché il trait Sommario è locale al nostro crate aggregatore.

Ma non possiamo implementare trait esterni su type esterni. Ad esempio, non possiamo implementare il trait Display su Vec<T> all’interno del nostro crate aggregatore perché Display e Vec<T> sono entrambi definiti nella libreria standard e non sono locali al nostro crate aggregatore. Questa restrizione fa parte di una proprietà chiamata coerenza (coherence), e più specificamente della regola dell’orfano (orphan rule), così chiamata perché il type genitore non è presente. Questa regola garantisce che il codice di altri non possa rompere il tuo codice e viceversa. Senza questa regola, due crate potrebbero implementare lo stesso trait per lo stesso type e Rust non saprebbe quale implementazione utilizzare.

Usare le Implementazioni Predefinite

A volte è utile avere un comportamento predefinito per alcuni o tutti i metodi in un trait invece di richiedere implementazioni per tutti i metodi su ogni type. Quindi, quando implementiamo il trait su un type particolare, possiamo mantenere o sovrascrivere il comportamento predefinito di ciascun metodo.

Nel Listato 10-14, specifichiamo una stringa predefinita per il metodo riassunto del trait Sommario invece di definire solo la firma del metodo, come abbiamo fatto nel Listato 10-12.

File: src/lib.rs
pub trait Sommario {
    fn riassunto(&self) -> String {
        String::from("(Leggi di più...)")
    }
}

pub struct Articolo {
    pub titolo: String,
    pub posizione: String,
    pub autore: String,
    pub contenuto: String,
}

impl Sommario for Articolo {}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto(&self) -> String {
        format!("{}: {}", self.nomeutente, self.contenuto)
    }
}
Listato 10-14: Definizione di un trait Sommario con un’implementazione predefinita del metodo riassunto

Per utilizzare un’implementazione predefinita per riassumere le istanze di Articolo, specifichiamo un blocco impl vuoto con impl Sommario for Articolo {}.

Anche se non definiamo più il metodo riassunto su Articolo direttamente, abbiamo fornito un’implementazione predefinita e specificato che Articolo implementa il trait Sommario. Di conseguenza, possiamo comunque chiamare il metodo riassunto su un’istanza di Articolo, in questo modo:


fn main() {
    let articolo = Articolo {
        titolo: String::from("I Penguins vincono la Stanley Cup!"),
        posizione: String::from("Pittsburgh, PA, USA"),
        autore: String::from("Iceburgh"),
        contenuto: String::from(
            "I Pittsburgh Penguins sono ancora una volta\
             la migliore squadra di hockey nella NHL.",
        ),
    };

    println!("Nuovo articolo disponibile! {}", articolo.riassunto());
}

Questo codice stampa Nuovo articolo disponibile! (Leggi di più...).

La creazione di un’implementazione predefinita non richiede alcuna modifica all’implementazione di Sommario su PostSocial nel Listato 10-13. Il motivo è che la sintassi per sovrascrivere un’implementazione predefinita è la stessa della sintassi per implementare un metodo di un trait che non ha un’implementazione predefinita.

Le implementazioni predefinite possono chiamare altri metodi nello stesso trait, anche se questi non hanno un’implementazione predefinita. In questo modo, un trait può fornire molte funzionalità utili e richiedere agli implementatori di specificarne solo una piccola parte. Ad esempio, potremmo definire il trait Sommario in modo che abbia un metodo riassunto_autore la cui implementazione è richiesta, e quindi definire un metodo riassunto con un’implementazione predefinita che chiama il metodo riassunto_autore:

pub trait Sommario {
    fn riassunto_autore(&self) -> String;

    fn riassunto(&self) -> String {
        format!("(Leggi di più da {}...)", self.riassunto_autore())
    }
}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto_autore(&self) -> String {
        format!("@{}", self.nomeutente)
    }
}

Per utilizzare questa versione di Sommario, dobbiamo definire riassunto_autore solo quando implementiamo il trait su un type:

pub trait Sommario {
    fn riassunto_autore(&self) -> String;

    fn riassunto(&self) -> String {
        format!("(Leggi di più da {}...)", self.riassunto_autore())
    }
}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto_autore(&self) -> String {
        format!("@{}", self.nomeutente)
    }
}

Dopo aver definito riassunto_autore, possiamo chiamare riassunto sulle istanze della struct PostSocial e l’implementazione predefinita di riassunto chiamerà la definizione di riassunto_autore che abbiamo fornito. Poiché abbiamo implementato riassunto_autore, il trait Sommario ci ha fornito il comportamento del metodo riassunto senza richiedere ulteriore codice. Ecco come appare:

use aggregatore::{self, PostSocial, Sommario};

fn main() {
    let post = PostSocial {
        nomeutente: String::from("horse_ebooks"),
        contenuto: String::from(
            "ovviamente, come probabilmente già sapete, gente",
        ),
        risposta: false,
        repost: false,
    };

    println!("1 nuovo post: {}", post.riassunto());
}

Questo codice stampa 1 nuovo post: (Leggi di più su @horse_ebooks...).

Nota che non è possibile chiamare l’implementazione predefinita da una implementazione sovrascritta dello stesso metodo.

Usare i Trait come Parametri

Ora che sai come definire e implementare i trait, possiamo esplorare come usarli per definire funzioni che accettano molti type diversi. Useremo il trait Sommario che abbiamo implementato sui type Articolo e PostSocial nel Listato 10-13 per definire una funzione notifica che chiama il metodo riassunto sul suo parametro elemento, che è di un type che implementa il trait Sommario. Per fare ciò, utilizziamo la sintassi impl Trait, in questo modo:

pub trait Sommario {
    fn riassunto(&self) -> String;
}

pub struct Articolo {
    pub titolo: String,
    pub posizione: String,
    pub autore: String,
    pub contenuto: String,
}

impl Sommario for Articolo {
    fn riassunto(&self) -> String {
        format!("{}, di {} ({})", self.titolo, self.autore, self.posizione)
    }
}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto(&self) -> String {
        format!("{}: {}", self.nomeutente, self.contenuto)
    }
}

pub fn notifica(elemento: &impl Sommario) {
    println!("Ultime notizie! {}", elemento.riassunto());
}

Invece di un type concreto per il parametro elemento, specifichiamo la parola chiave impl e il nome del trait. Questo parametro accetta qualsiasi type che implementi il trait specificato. Nel corpo di notifica, possiamo chiamare qualsiasi metodo su elemento che provenga dal trait Sommario, come riassunto. Possiamo chiamare notifica e passare qualsiasi istanza di Articolo o PostSocial. Il codice che chiama la funzione con qualsiasi altro type, come String o i32, non verrà compilato perché questi type non implementano Sommario.

Sintassi del Vincolo di Trait

La sintassi impl Trait funziona per i casi più semplici, ma in realtà è solo una sintassi semplificata di una forma più lunga nota come vincolo del tratto (trait bound); si presenta così:

pub fn notifica<T: Sommario>(elemento: &T) {
    println!("Ultime notizie! {}", elemento.riassunto());
}

Questa forma più lunga è equivalente all’esempio della sezione precedente, ma è più dettagliata. Posizioniamo il vincolo di trait con la dichiarazione del parametro di type generico dopo i due punti e tra parentesi angolari.

La sintassi impl Trait è comoda e consente di scrivere codice più conciso nei casi semplici, mentre la sintassi più completa del vincolo di trait può esprimere una maggiore complessità in altri casi. Ad esempio, possiamo avere due parametri che implementano Sommario. Con la sintassi impl Trait, ciò si ottiene in questo modo:

pub fn notifica(elemento1: &impl Sommario, elemento2: &impl Sommario) {

L’utilizzo di impl Trait è appropriato se vogliamo che questa funzione consenta a elemento1 e elemento2 di avere type diversi (purché entrambi i type implementino Sommario). Tuttavia, se vogliamo forzare entrambi i parametri ad avere lo stesso type, dobbiamo usare un vincolo di trait, in questo modo:

pub fn notifica<T: Sommario>(elemento1: &T, elemento2: &T) {

Il type generico T specificato come type dei parametri elemento1 e elemento2 vincola la funzione in modo che il type concreto del valore passato come argomento per elemento1 e elemento2 debba essere lo stesso.

Specificare più Vincoli di Trait con la Sintassi +

Possiamo anche specificare più di un vincolo di trait. Supponiamo di voler che notifica usi sia la formattazione di visualizzazione, fornita dal trait Display, sia che usi riassunto su elemento: specifichiamo nella definizione di notifica che elemento deve implementare sia Display che Sommario. Possiamo farlo utilizzando la sintassi +:

pub fn notifica(elemento: &(impl Sommario + Display)) {

La sintassi + è valida anche con i vincoli di trait sui type generici:

pub fn notifica<T: Sommario + Display>(elemento: &T) {

Con i due vincoli di trait specificati, il corpo di notifica può chiamare riassunto e utilizzare {} per formattare elemento.

Specificare i Vincoli di Trait con le Clausole where

L’utilizzo di troppi vincoli di trait ha i suoi svantaggi. Ogni generico ha i suoi vincoli di trait, quindi le funzioni con più parametri di type generico possono contenere molte informazioni sui vincoli di trait tra il nome della funzione e il suo elenco di parametri, rendendo la firma della funzione difficile da leggere. Per questo motivo, Rust ha una sintassi alternativa per specificare i vincoli di trait all’interno di una clausola where dopo la firma della funzione. Quindi, invece di scrivere:

fn una_funzione<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

possiamo usare una clausola where, in questo modo:

fn una_funzione<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    unimplemented!()
}

La firma di questa funzione è meno confusionaria: il nome della funzione, l’elenco dei parametri e il type di ritorno sono vicini, come in una funzione senza molti vincoli di trait.

Restituire Type che Implementano Trait

Possiamo anche usare la sintassi impl Trait nella posizione di ritorno per restituire un valore di un type che implementa un trait, come mostrato qui:

pub trait Sommario {
    fn riassunto(&self) -> String;
}

pub struct Articolo {
    pub titolo: String,
    pub posizione: String,
    pub autore: String,
    pub contenuto: String,
}

impl Sommario for Articolo {
    fn riassunto(&self) -> String {
        format!("{}, di {} ({})", self.titolo, self.autore, self.posizione)
    }
}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto(&self) -> String {
        format!("{}: {}", self.nomeutente, self.contenuto)
    }
}

fn riassumibile() -> impl Sommario {
    PostSocial {
        nomeutente: String::from("horse_ebooks"),
        contenuto: String::from(
            "ovviamente, come probabilmente già sapete, gente",
        ),
        risposta: false,
        repost: false,
    }
}

Utilizzando impl Sommario come type di ritorno, specifichiamo che la funzione riassumibile restituisce un type che implementa il trait Sommario senza nominare il type concreto. In questo caso, riassumibile restituisce un PostSocial, ma il codice che chiama questa funzione non ha bisogno di saperlo.

La possibilità di specificare un type di ritorno solo tramite il trait che implementa è particolarmente utile nel contesto di chiusure (closure) e iteratori, che tratteremo nel Capitolo 13. Chiusure e iteratori creano type che solo il compilatore conosce o type che sono molto lunghi da specificare. La sintassi impl Trait consente di specificare in modo conciso che una funzione restituisca un type che implementa il trait Iterator senza dover scrivere un type molto lungo.

Tuttavia, è possibile utilizzare impl Trait solo se si restituisce un singolo type. Ad esempio, questo codice che restituisce un Articolo o un PostSocial con il type di ritorno specificato come impl Sommario non funzionerebbe:

pub trait Sommario {
    fn riassunto(&self) -> String;
}

pub struct Articolo {
    pub titolo: String,
    pub posizione: String,
    pub autore: String,
    pub contenuto: String,
}

impl Sommario for Articolo {
    fn riassunto(&self) -> String {
        format!("{}, di {} ({})", self.titolo, self.autore, self.posizione)
    }
}

pub struct PostSocial {
    pub nomeutente: String,
    pub contenuto: String,
    pub risposta: bool,
    pub repost: bool,
}

impl Sommario for PostSocial {
    fn riassunto(&self) -> String {
        format!("{}: {}", self.nomeutente, self.contenuto)
    }
}

fn riassumibile(switch: bool) -> impl Sommario {
    if switch {
        Articolo {
            titolo: String::from(
                "I Penguins vincono la Stanley Cup!",
            ),
            posizione: String::from("Pittsburgh, PA, USA"),
            autore: String::from("Iceburgh"),
            contenuto: String::from(
                "I Pittsburgh Penguins sono ancora una volta la migliore squadra di hockey nella NHL.",
            ),
        }
    } else {
        PostSocial {
            nomeutente: String::from("horse_ebooks"),
            contenuto: String::from(
                "ovviamente, come probabilmente già sapete, gente",
            ),
            risposta: false,
            riposta: false,
        }
    }
}

Restituire un Articolo o un PostSocial non è consentito a causa di restrizioni relative all’implementazione della sintassi impl Trait nel compilatore. Spiegheremo come scrivere una funzione con questo comportamento nella sezione “Usare gli Oggetti Trait per Astrarre Comportamenti Condivisi” del Capitolo 18.

Utilizzare Vincoli di Trait per Implementare Metodi in Modo Condizionale

Utilizzando un vincolo di trait con un blocco impl che utilizza parametri di type generico, possiamo implementare metodi in modo condizionale per i type che implementano i trait specificati. Ad esempio, il type Coppia<T> nel Listato 10-15 implementa sempre la funzione new per restituire una nuova istanza di Coppia<T> (abbiamo menzionato nella sezione “Metodi” del Capitolo 5 che Self è un alias di type per il type del blocco impl, che in questo caso è Coppia<T>). Ma nel blocco impl successivo, Coppia<T> implementa il metodo mostra_comparazione solo se il suo type interno T implementa il trait PartialOrd che abilita il confronto e il trait Display che abilita la stampa.

File: src/lib.rs
use std::fmt::Display;

struct Coppia<T> {
    x: T,
    y: T,
}

impl<T> Coppia<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Coppia<T> {
    fn mostra_comparazione(&self) {
        if self.x >= self.y {
            println!("Il membro più grande è x = {}", self.x);
        } else {
            println!("Il membro più grande è y = {}", self.y);
        }
    }
}
Listato 10-15: Implementazione condizionale di metodi su un type generico in base ai vincoli di trait

Possiamo anche implementare in modo condizionale un trait per qualsiasi type che implementa un altro trait. Le implementazioni di un trait su qualsiasi type che soddisfi i vincoli di trait sono chiamate implementazioni generali (blanket implementations) e sono ampiamente utilizzate nella libreria standard di Rust. Ad esempio, la libreria standard implementa il trait ToString su qualsiasi type che implementi il trait Display. Il blocco impl nella libreria standard è simile a questo codice:

impl<T: Display> ToString for T {
    // --taglio--
}

Poiché la libreria standard ha questa implementazione generale, possiamo chiamare il metodo to_string definito dal trait ToString su qualsiasi type che implementi il trait Display. Ad esempio, possiamo trasformare gli integer nei loro corrispondenti valori String in questo modo, perché gli integer implementano Display:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Le implementazioni generali compaiono nella documentazione per il trait in questione nella sezione “Implementatori” (Implementors).

I trait e i vincoli dei trait ci consentono di scrivere codice che utilizza parametri di type generico per ridurre le duplicazioni, ma anche di specificare al compilatore che desideriamo che il type generico abbia un comportamento particolare. Il compilatore può quindi utilizzare le informazioni sui vincoli di trait per verificare che tutti i type concreti utilizzati nel nostro codice forniscano il comportamento corretto. Nei linguaggi a tipizzazione dinamica, otterremmo un errore durante l’esecuzione se chiamassimo un metodo su un type che non lo definisce. Ma Rust sposta questi errori in fase di compilazione, quindi siamo costretti a correggere i problemi prima ancora che il nostro codice possa essere eseguito. Inoltre, non dobbiamo scrivere codice che verifichi il comportamento durante l’esecuzione, perché lo abbiamo già verificato in fase di compilazione. Ciò migliora le prestazioni senza dover rinunciare alla flessibilità dei type generici.