Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Corso Avanzato su Traits

Abbiamo introdotto i traits nella sezione “Traits: Definizione di Comportamenti Condivisi” del Capitolo 10, ma non abbiamo discusso i dettagli più avanzati. Ora che conosci meglio Rust, possiamo entrare nei dettagli più specifici. Definizione di Tipi Segnaposto nei Traits con Tipi Associati

I tipi associati collegano un tipo segnaposto a un trait in modo che le definizioni dei metodi del trait possano utilizzare questi tipi segnaposto nelle loro firme. L’implementatore di un trait specifica il tipo concreto da utilizzare invece del tipo segnaposto per l’implementazione specifica. In questo modo, possiamo definire un trait che utilizza alcuni tipi senza dover sapere esattamente quali siano quei tipi fino a quando il trait non viene implementato.

Abbiamo descritto la maggior parte delle funzionalità avanzate in questo capitolo come poco utilizzate. I tipi associati sono in una posizione intermedia: vengono utilizzati più raramente rispetto alle funzionalità spiegate nel resto del libro ma più comunemente rispetto a molte delle altre funzionalità discusse in questo capitolo.

Un esempio di un trait con un tipo associato è il trait Iterator che fornisce la libreria standard. Il tipo associato si chiama Item e rappresenta il tipo dei valori su cui il tipo che implementa il trait Iterator sta iterando. La definizione del trait Iterator è come mostrato in Lista 19-12.

rust

pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;
}

Il tipo Item è un segnaposto, e la definizione del metodo next mostra che restituirà valori di tipo OptionSelf::Item. Gli implementatori del trait Iterator specificano il tipo concreto per Item, e il metodo next restituirà un’Opzione contenente un valore di quel tipo concreto.

I tipi associati potrebbero sembrare un concetto simile ai generici, in quanto questi ultimi ci permettono di definire una funzione senza specificare quali tipi può gestire. Per esaminare la differenza tra i due concetti, guarderemo a un’implementazione del trait Iterator su un tipo chiamato Counter che specifica che il tipo Item è u32:

rust

impl Iterator for Counter {
type Item = u32;

fn next(&mut self) -> Option<Self::Item> {
// --snip--

Questa sintassi sembra paragonabile a quella dei generici. Quindi perché non definire il trait Iterator con i generici, come mostrato in Lista 19-13?

rust

pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}

La differenza è che quando si utilizzano i generici, come in Lista 19-13, dobbiamo annotare i tipi in ogni implementazione; poiché possiamo anche implementare Iterator<String> per Counter o per qualsiasi altro tipo, potremmo avere più implementazioni di Iterator per Counter. In altre parole, quando un trait ha un parametro generico, può essere implementato per un tipo più volte, cambiando i tipi concreti dei parametri generici ogni volta. Quando usiamo il metodo next su Counter, dovremmo fornire annotazioni di tipo per indicare quale implementazione di Iterator vogliamo utilizzare.

Con i tipi associati, non dobbiamo annotare i tipi perché non possiamo implementare un trait su un tipo più volte. In Lista 19-12 con la definizione che utilizza tipi associati, possiamo scegliere solo quale sarà il tipo di Item una volta, perché può esserci solo una impl Iterator per Counter. Non dobbiamo specificare che vogliamo un iteratore di valori u32 ovunque chiamiamo next su Counter.

I tipi associati diventano anche parte del contratto del trait: gli implementatori del trait devono fornire un tipo per sostituire il segnaposto del tipo associato. I tipi associati hanno spesso un nome che descrive come il tipo verrà utilizzato, e documentare il tipo associato nella documentazione dell’API è una pratica consigliata.

Corso Avanzato su Traits

Sintassi Completamente Qualificata per la Dissambiguazione: Chiamare Metodi con lo Stesso Nome

In Rust, nulla impedisce a un trait di avere un metodo con lo stesso nome di un altro metodo di un altro trait, né Rust ti impedisce di implementare entrambi i trait su un unico tipo. È anche possibile implementare un metodo direttamente sul tipo con lo stesso nome dei metodi dei trait.

Quando si chiamano metodi con lo stesso nome, è necessario dire a Rust quale si desidera utilizzare. Considera il codice nella Lista 19-16 dove abbiamo definito due trait, Pilota e Mago, entrambi con un metodo chiamato volare. Poi implementiamo entrambi i trait su un tipo Umano che ha già un metodo chiamato volare implementato su di esso. Ogni metodo volare fa qualcosa di diverso.

rust

trait Pilota {
fn volare(&self);
}
trait Mago {
fn volare(&self);
} struct Umano; impl Pilota for Umano {
fn volare(&self) {
println!("Parla il tuo capitano.");
}
} impl Mago for Umano {
fn volare(&self) {
println!("In alto!");
}
}

impl Umano {
fn volare(&self) {
println!("*agitando furiosamente le braccia*");
}
}

Quando chiamiamo volare su un’istanza di Umano, il compilatore predefinisce la chiamata al metodo implementato direttamente sul tipo, come mostrato nella Lista 19-17.

rust

fn main() {
let persona = Umano;
persona.volare();
}

Eseguendo questo codice verrà stampato agitando furiosamente le braccia, mostrando che Rust ha chiamato il metodo volare implementato direttamente su Umano.

Per chiamare i metodi volare sia dal trait Pilota che dal trait Mago, è necessario utilizzare una sintassi più esplicita per specificare quale metodo volare intendiamo usare. La Lista 19-18 dimostra questa sintassi.

rust

fn main() {
let persona = Umano;
Pilota::volare(&persona);
Mago::volare(&persona);
persona.volare();
}

Specificare il nome del trait prima del nome del metodo chiarisce a Rust quale implementazione di volare vogliamo chiamare. Potremmo anche scrivere Umano::volare(&persona), che è equivalente a persona.volare() che abbiamo usato nella Lista 19-18, ma questo è un po’ più lungo da scrivere se non abbiamo bisogno di dissambiguare.

Eseguendo questo codice stampa quanto segue:

less

$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
Parla il tuo capitano.
In alto!
*agitando furiosamente le braccia*

Poiché il metodo volare prende un parametro self, se avessimo due tipi che implementano entrambi un trait, Rust potrebbe capire quale implementazione di un trait utilizzare in base al tipo di self.

Tuttavia, le funzioni associate che non sono metodi non hanno un parametro self. Quando ci sono più tipi o trait che definiscono funzioni non metodi con lo stesso nome di funzione, Rust non sa sempre quale tipo intendi a meno che tu non utilizzi la sintassi completamente qualificata. Ad esempio, nella Lista 19-19 creiamo un trait per un rifugio per animali che vuole chiamare tutti i cuccioli Spot. Creiamo un trait Animal con una funzione associata non metodo baby_name. Il trait Animal viene implementato per la struttura Cane, su cui forniamo anche una funzione associata non metodo baby_name direttamente.

rust

trait Animale {
fn baby_name() -> String;
}
struct Cane; impl Cane {
fn baby_name() -> String {
String::from("Spot")
}
} impl Animale for Cane {
fn baby_name() -> String {
String::from("cucciolo")
}
}

fn main() {
println!("Un cucciolo di cane si chiama {}", Cane::baby_name());
}

Implementiamo il codice per chiamare tutti i cuccioli Spot nella funzione associata baby_name che è definita su Cane. Il tipo Cane implementa anche il trait Animale, che descrive le caratteristiche che tutti gli animali hanno. I cuccioli di cane si chiamano cuccioli, ed è espresso nell’implementazione del trait Animale su Cane nella funzione baby_name associata al trait Animale.

In main, chiamiamo la funzione Cane::baby_name, che chiama la funzione associata definita su Cane direttamente. Questo codice stampa quanto segue:

less

$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
Un cucciolo di cane si chiama Spot

Questo output non è quello che volevamo. Vogliamo chiamare la funzione baby_name che fa parte del trait Animal che abbiamo implementato su Cane in modo che il codice stampi Un cucciolo di cane si chiama un cucciolo. La tecnica di specificare il nome del trait che abbiamo usato nella Lista 19-18 non aiuta qui; se cambiamo main al codice nella Lista 19-20, otterremo un errore di compilazione.

rust

fn main() {
println!("Un cucciolo di cane si chiama {}", Animale::baby_name());
}

Poiché Animale::baby_name non ha un parametro self, e potrebbero esserci altri tipi che implementano il trait Animale, Rust non può capire quale implementazione di Animale::baby_name vogliamo utilizzare. Otterremo questo errore del compilatore:

rust

$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animale::baby_name` defined here
...
20 | println!("Un cucciolo di cane si chiama {}", Animale::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("Un cucciolo di cane si chiama {}", <Cane as Animale>::baby_name());
| +++++++ +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` due to previous error

Per dissambiguare e dire a Rust che vogliamo utilizzare l’implementazione di Animale per Cane anziché l’implementazione di Animale per un altro tipo, dobbiamo utilizzare la sintassi completamente qualificata. La Lista 19-21 mostra come utilizzare la sintassi completamente qualificata.

rust

fn main() {
println!("Un cucciolo di cane si chiama {}", <Cane as Animale>::baby_name());
}

Stiamo fornendo a Rust una annotazione di tipo all’interno delle parentesi angolate, che indica che vogliamo chiamare il metodo baby_name dal trait Animale come implementato su Cane dicendo che vogliamo trattare il tipo Cane come un Animale per questa chiamata di funzione. Questo codice stamperà ora ciò che vogliamo:

less

$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
Un cucciolo di cane si chiama un cucciolo

In generale, la sintassi completamente qualificata è definita come segue:

rust

<Tipo come Trait>::funzione(receiver_se_metodo, prossimo_argomento, ...);

Per le funzioni associate che non sono metodi, non ci sarebbe un receiver: ci sarebbe solo l’elenco degli altri argomenti. Potresti utilizzare la sintassi completamente qualificata ovunque chiami funzioni o metodi. Tuttavia, ti è consentito omettere qualsiasi parte di questa sintassi che Rust può capire da altre informazioni nel programma. È necessario utilizzare questa sintassi più verbosa solo nei casi in cui ci sono più implementazioni che utilizzano lo stesso nome e Rust ha bisogno di aiuto per identificare quale implementazione vuoi chiamare.

Corso Avanzato su Traits

Utilizzo dei Supertraits per Richiedere la Funzionalità di un Trait all’interno di un Altro Trait

A volte potresti scrivere una definizione di trait che dipende da un altro trait: per far sì che un tipo implementi il primo trait, desideri richiedere che quel tipo implementi anche il secondo trait. Fai questo in modo che la definizione del tuo trait possa utilizzare gli elementi associati del secondo trait. Il trait su cui si basa la definizione del tuo trait viene chiamato supertrait del tuo trait.

Ad esempio, diciamo che vogliamo creare un trait OutlinePrint con un metodo outline_print che stampi un valore dato formattato in modo che sia incorniciato da asterischi. Cioè, dato una struttura Punto che implementa il trait della libreria standard Display per risultare in (x, y), quando chiamiamo outline_print su un’istanza di Punto che ha 1 per x e 3 per y, dovrebbe stampare quanto segue:

plaintext

**********
* *
* (1, 3) *
* *
**********

Nell’implementazione del metodo outline_print, vogliamo utilizzare la funzionalità del trait Display. Pertanto, dobbiamo specificare che il trait OutlinePrint funzionerà solo per i tipi che implementano anche Display e forniscono la funzionalità di cui OutlinePrint ha bisogno. Possiamo farlo nella definizione del trait specificando OutlinePrint: Display. Questa tecnica è simile all’aggiunta di un vincolo di trait al trait. Ecco un’implementazione del trait OutlinePrint.

rust

use std::fmt;

trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}

Poiché abbiamo specificato che OutlinePrint richiede il trait Display, possiamo usare la funzione to_string che è implementata automaticamente per qualsiasi tipo che implementa Display. Se provassimo a usare to_string senza aggiungere i due punti e specificare il trait Display dopo il nome del trait, otterremmo un errore dicendo che non è stato trovato alcun metodo chiamato to_string per il tipo &Self nell’ambito corrente.

Vediamo cosa succede quando proviamo a implementare OutlinePrint su un tipo che non implementa Display, come la struttura Punto:

rust

struct Punto {
x: i32,
y: i32,
}

impl OutlinePrint for Punto {}

Otteniamo un errore dicendo che Display è richiesto ma non è implementato:

rust

error[E0277]: `Punto` non implementa `std::fmt::Display`
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Punto {}
| ^^^^^^^^^^^^ `Punto` non può essere formattato con il formatter predefinito
|
= aiuto: il trait `std::fmt::Display` non è implementato per `Punto`
= nota: nei format strings potresti essere in grado di usare `{:?}` (o {:#?} per la pretty-print) invece
note: richiesto da un vincolo in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ richiesto da questo vincolo in `OutlinePrint`

Per risolvere questo, implementiamo Display su Punto e soddisfiamo il vincolo che OutlinePrint richiede, in questo modo:

rust

use std::fmt;

impl fmt::Display for Punto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}

Quindi implementare il trait OutlinePrint su Punto compilerà correttamente, e possiamo chiamare outline_print su un’istanza di Punto per visualizzarla all’interno di un contorno di asterischi.

1 Comment

Leave a Reply

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