Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories
 Corso Gratuito di Programmazione Rust Lezione 035 – Tratti: Definire il comportamento condiviso

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Traits: Definizione di Comportamenti Condivisi

Un trait definisce le funzionalità che un particolare tipo ha e può condividere con altri tipi. Possiamo utilizzare i trait per definire comportamenti condivisi in modo astratto. I vincoli dei trait ci permettono di specificare che un tipo generico può essere qualsiasi tipo che ha determinati comportamenti.

kotlin

Nota: I trait sono simili a una caratteristica spesso chiamata interfacce in altri linguaggi, anche se con alcune differenze.

Definire un Trait

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

Ad esempio, diciamo che abbiamo diverse strutture che contengono vari tipi e quantità di testo: una struttura NewsArticle che contiene una storia di notizie archiviata in una posizione specifica e un Tweet che può avere al massimo 280 caratteri insieme ai metadati che indicano se era un nuovo tweet, un retweet o una risposta a un altro tweet.

Vogliamo creare una libreria di aggregazione multimediale chiamata aggregator che può visualizzare riassunti di dati che potrebbero essere memorizzati in un’istanza di NewsArticle o Tweet. Per fare ciò, abbiamo bisogno di un riassunto da ciascun tipo e richiederemo quel riassunto chiamando un metodo summarize su un’istanza. L’Esempio 10-12 mostra la definizione di un trait Summary pubblico che esprime questo comportamento.

rust

pub trait Summary {
fn summarize(&self) -> String;
}

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

Dopo la firma del metodo, invece di fornire un’implementazione all’interno delle parentesi graffe, utilizziamo un punto e virgola. Ogni tipo che implementa questo trait deve fornire il proprio comportamento personalizzato per il corpo del metodo. Il compilatore garantirà che qualsiasi tipo che abbia il trait Summary avrà il metodo summarize definito con questa firma esattamente.

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

Implementare un Trait su un Tipo

Ora che abbiamo definito le firme desiderate dei metodi del trait Summary, possiamo implementarlo sui tipi nella nostra libreria di aggregazione multimediale. L’Esempio 10-13 mostra un’implementazione del trait Summary sulla struttura NewsArticle che utilizza il titolo, l’autore e la posizione per creare il valore di ritorno di summarize. Per la struttura Tweet, definiamo summarize come il nome utente seguito dall’intero testo del tweet, assumendo che il contenuto del tweet sia già limitato a 280 caratteri.

rust

pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
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)
}
}

Implementare un trait su un tipo è simile all’implementazione di metodi regolari. La differenza è che dopo impl, mettiamo il nome del trait che vogliamo implementare, quindi usiamo la parola chiave for e quindi specifichiamo il nome del tipo per cui vogliamo implementare il trait. All’interno del blocco impl, mettiamo le firme dei metodi che la definizione del trait ha definito. Invece di aggiungere un punto e virgola dopo ogni firma, usiamo le parentesi graffe e compiliamo il corpo del metodo con il comportamento specifico che vogliamo che i metodi del trait abbiano per quel particolare tipo.

Ora che la libreria ha implementato il trait Summary su NewsArticle e Tweet, gli utenti della crate possono chiamare i metodi del trait su istanze di NewsArticle e Tweet allo stesso modo in cui chiamiamo i metodi regolari. L’unica differenza è che l’utente deve portare il trait nello scope così come i tipi. Ecco un esempio di come una crate binaria potrebbe utilizzare la nostra crate di libreria di aggregazione:

rust

use aggregator::{Summary, Tweet}; fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};

println!("1 new tweet: {}", tweet.summarize());
}

Questo codice stampa 1 nuovo tweet: horse_ebooks: of course, as you probably already know, people.

Altre crate che dipendono dalla crate aggregator possono anche portare il trait Summary nello scope per implementare Summary sui propri tipi. Una restrizione da notare è che possiamo implementare un trait su un tipo solo se almeno uno dei trait o del tipo è locale alla nostra crate. Ad esempio, possiamo implementare i trait della libreria standard come Display su un tipo personalizzato come Tweet come parte della nostra funzionalità di crate di aggregazione, perché il tipo Tweet è locale alla nostra crate di aggregator. Possiamo anche implementare Summary su Vec<T> nella nostra crate di aggregator, perché il trait Summary è locale alla nostra crate di aggregator.

Ma non possiamo implementare i trait esterni sui tipi esterni. Ad esempio, non possiamo implementare il trait Display su Vec<T> all’interno della nostra crate di aggregator, perché Display e Vec<T> sono entrambi definiti nella libreria standard e non sono locali alla nostra crate di aggregator. Questa restrizione fa parte di una proprietà chiamata coerenza, e più specificamente della regola dell’orfano, così chiamata perché il tipo padre non è presente. Questa regola garantisce che il codice altrui non possa rompere il nostro codice e viceversa. Senza la regola, due crate potrebbero implementare lo stesso trait per lo stesso tipo e Rust non saprebbe quale implementazione utilizzare.

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 tipo. Quindi, mentre implementiamo il trait su un tipo particolare, possiamo mantenere o sovrascrivere il comportamento predefinito di ciascun metodo.

Nell’Esempio 10-14 specifichiamo una stringa predefinita per il metodo summarize del trait Summary anziché definire solo la firma del metodo, come abbiamo fatto nell’Esempio 10-12.

rust

pub trait Summary {
fn summarize(&self) -> String {
String::from("(Continua a leggere...)")
}
}

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

Anche se non stiamo più definendo il metodo summarize direttamente su NewsArticle, abbiamo fornito un’implementazione predefinita e specificato che NewsArticle implementa il trait Summary. Di conseguenza, possiamo comunque chiamare il metodo summarize su un’istanza di NewsArticle, come questo:

rust

let article = NewsArticle {
headline: String::from("I pinguini vincono il Campionato della Stanley Cup!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"I Pittsburgh Penguins sono ancora una volta la migliore \
squadra di hockey nella NHL.",
),
};

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

Questo codice stampa Nuovo articolo disponibile! (Continua a leggere…).

La creazione di un’implementazione predefinita non richiede di modificare nulla riguardo all’implementazione di Summary su Tweet nell’Esempio 10-13. Il motivo è che la sintassi per sovrascrivere un’implementazione predefinita è la stessa della sintassi per implementare un metodo del trait che non ha un’implementazione predefinita.

Le implementazioni predefinite possono chiamare altri metodi nello stesso trait, anche se quegli altri metodi non hanno un’implementazione predefinita. In questo modo, un trait può fornire molta funzionalità utile e richiedere solo agli implementatori di specificare una piccola parte di essa. Ad esempio, potremmo definire il trait Summary per avere un metodo summarize_author la cui implementazione è richiesta, e quindi definire un metodo summarize che ha un’implementazione predefinita che chiama il metodo summarize_author:

rust

pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
format!("(Continua a leggere da {}...)", self.summarize_author())
}
}

Per utilizzare questa versione di Summary, dobbiamo solo definire summarize_author quando implementiamo il trait su un tipo:

rust

impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}

Dopo aver definito summarize_author, possiamo chiamare summarize su istanze della struttura Tweet e l’implementazione predefinita di summarize chiamerà la definizione di summarize_author che abbiamo fornito. Poiché abbiamo implementato summarize_author, il trait Summary ci ha dato il comportamento del metodo summarize senza richiederci di scrivere altro codice.

rust

let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"ovviamente, come probabilmente già sai, la gente",
),
reply: false,
retweet: false,
};

println!("1 nuovo tweet: {}", tweet.summarize());

Questo codice stampa 1 nuovo tweet: (Continua a leggere da @horse_ebooks…).

Nota che non è possibile chiamare l’implementazione predefinita da un’implementazione di sovrascrittura di quel stesso metodo.

Utilizzo dei Traits come Parametri

Ora che sai come definire e implementare i traits, possiamo esplorare come utilizzarli per definire funzioni che accettano molti tipi diversi. Utilizzeremo il trait Summary che abbiamo implementato sui tipi NewsArticle e Tweet nell’Esempio 10-13 per definire una funzione notify che chiama il metodo summarize sul suo parametro item, che è di qualche tipo che implementa il trait Summary. Per fare ciò, utilizziamo la sintassi impl Trait, così:

rust

pub fn notify(item: &impl Summary) {
println!("Notizia dell'ultima ora! {}", item.summarize());
}

Invece di un tipo concreto per il parametro item, specifichiamo la parola chiave impl e il nome del trait. Questo parametro accetta qualsiasi tipo che implementa il trait specificato. Nel corpo di notify, possiamo chiamare qualsiasi metodo su item che proviene dal trait Summary, come summarize. Possiamo chiamare notify e passare qualsiasi istanza di NewsArticle o Tweet. Il codice che chiama la funzione con qualsiasi altro tipo, come una String o un i32, non verrà compilato perché quei tipi non implementano Summary.

Sintassi del Trait Bound

La sintassi impl Trait funziona per casi semplici ma in realtà è uno zucchero sintattico per una forma più lunga nota come trait bound; si presenta così:

rust

pub fn notify<T: Summary>(item: &T) {
println!("Notizia dell'ultima ora! {}", item.summarize());
}

Questa forma più lunga è equivalente all’esempio nella sezione precedente ma è più verbosa. Poniamo i vincoli del trait con la dichiarazione del parametro del tipo generico dopo i due punti e all’interno delle parentesi angolari.

La sintassi impl Trait è conveniente e consente di scrivere codice più conciso nei casi semplici, mentre la sintassi più completa del trait bound può esprimere una maggiore complessità in altri casi. Ad esempio, possiamo avere due parametri che implementano Summary. Farlo con la sintassi impl Trait appare così:

rust

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

L’utilizzo di impl Trait è appropriato se vogliamo che questa funzione permetta a item1 e item2 di avere tipi diversi (purché entrambi i tipi implementino Summary). Se vogliamo forzare entrambi i parametri ad avere lo stesso tipo, tuttavia, dobbiamo usare un trait bound, così:

rust

pub fn notify<T: Summary>(item1: &T, item2: &T) {

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

Specificare Più Vincoli di Trait con la Sintassi +

Possiamo anche specificare più di un vincolo di trait. Diciamo che vogliamo che notify utilizzi la formattazione display così come summarize su item: specifichiamo nella definizione di notify che item deve implementare sia Display che Summary. Possiamo farlo usando la sintassi + così:

rust

pub fn notify(item: &(impl Summary + Display)) {

La sintassi + è anche valida con i vincoli dei trait su tipi generici:

rust

pub fn notify<T: Summary + Display>(item: &T) {

Con i due vincoli di trait specificati, il corpo di notify può chiamare summarize e utilizzare {} per formattare item.

Vincoli di Trait più Chiari con 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 tipo generico possono contenere molte informazioni sui vincoli di trait tra il nome della funzione e l’elenco dei parametri, rendendo difficile leggere la firma della funzione. 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 anziché scrivere questo:

rust

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

possiamo usare una clausola where, così:

rust

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

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

Restituire Tipi che Implementano i Trait

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

rust

fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"ovviamente, come probabilmente già sai, la gente",
),
reply: false,
retweet: false,
}
}

Utilizzando impl Summary per il tipo di ritorno, specifichiamo che la funzione returns_summarizable restituisce un qualche tipo che implementa il trait Summary senza specificare il tipo concreto. In questo caso, returns_summarizable restituisce un Tweet, ma il codice che chiama questa funzione non ha bisogno di saperlo.

La capacità di specificare un tipo di ritorno solo dal trait che implementa è particolarmente utile nel contesto di chiusure e iteratori, che tratteremo nel Capitolo 13. Le chiusure e gli iteratori creano tipi che solo il compilatore conosce o tipi che sono molto lunghi da specificare. La sintassi impl Trait consente di specificare in modo conciso che una funzione restituisce un qualche tipo che implementa il trait Iterator senza dover scrivere un tipo molto lungo.

Tuttavia, è possibile utilizzare impl Trait solo se si restituisce un singolo tipo. Ad esempio, questo codice che restituisce sia un NewsArticle che un Tweet con il tipo di ritorno specificato come impl Summary non funzionerebbe:

rust

[Questo codice non compila!]
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"I pinguini vincono il Campionato della Stanley Cup!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"I Pittsburgh Penguins sono ancora una volta la migliore \
squadra di hockey nella NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"ovviamente, come probabilmente già sai, la gente",
),
reply: false,
retweet: false,
}
}
}

Restituire sia un NewsArticle che un Tweet non è consentito a causa delle restrizioni legate alla sintassi impl Trait implementata nel compilatore. Vedremo come scrivere una funzione con questo comportamento nella sezione “Utilizzo degli Oggetti del Trait che Consentono Valori di Diversi Tipi” del Capitolo 17.

Utilizzo dei Vincoli di Trait per Implementare Condizionalmente i Metodi

Utilizzando un vincolo di trait con un blocco impl che utilizza parametri di tipo generico, possiamo implementare metodi condizionalmente per tipi che implementano i trait specificati. Ad esempio, il tipo Pair<T> nell’Esempio 10-15 implementa sempre la nuova funzione per restituire una nuova istanza di Pair<T> (ricordiamo dalla sezione “Definizione dei Metodi” del Capitolo 5 che Self è un alias del tipo del blocco impl, che in questo caso è Pair<T>). Ma nel blocco impl successivo, Pair<T> implementa solo il metodo cmp_display se il suo tipo interno T implementa il trait PartialOrd che abilita il confronto e il trait Display che abilita la stampa.

rust

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!("Il membro più grande è x = {}", self.x);
} else {
println!("Il membro più grande è y = {}", self.y);
}
}
}

Possiamo anche implementare condizionalmente un trait per qualsiasi tipo che implementi un altro trait. Le implementazioni di un trait su qualsiasi tipo che soddisfa i vincoli del trait sono chiamate implementazioni blanket e sono ampiamente utilizzate nella libreria standard di Rust. Ad esempio, la libreria standard implementa il trait ToString su qualsiasi tipo che implementa il trait Display. Il blocco impl nella libreria standard appare simile a questo codice:

rust

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

Poiché la libreria standard ha questa implementazione blanket, possiamo chiamare il metodo to_string definito dal trait ToString su qualsiasi tipo che implementa il trait Display. Ad esempio, possiamo trasformare gli interi nei loro valori di tipo String in questo modo perché gli interi implementano Display:

rust

let s = 3.to_string();

Le implementazioni blanket appaiono nella documentazione del trait nella sezione “Implementors”.

I Traits e i vincoli di trait ci permettono di scrivere codice che utilizza parametri di tipo generico per ridurre la duplicazione ma specifica anche al compilatore che vogliamo che il tipo generico abbia un determinato comportamento. Il compilatore può quindi utilizzare le informazioni sui vincoli del trait per verificare che tutti i tipi concreti utilizzati con il nostro codice forniscano il comportamento corretto. Nei linguaggi di programmazione con tipizzazione dinamica, otterremmo un errore a runtime se chiamassimo un metodo su un tipo che non definisse il metodo. Ma Rust sposta questi errori al tempo di compilazione in modo che siamo costretti a risolvere i problemi prima che il nostro codice possa essere eseguito. Inoltre, non dobbiamo scrivere codice che controlli il comportamento a runtime perché abbiamo già controllato a tempo di compilazione. Farlo migliora le prestazioni senza dover rinunciare alla flessibilità dei generici.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *