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

Trait Avanzati

Abbiamo già visto i trait in Trait: Definire il Comportamento Condiviso con i Trait nel Capitolo 10, ma non abbiamo trattato i dettagli più avanzati. Ora che sai di più su Rust, possiamo mettere le mani in pasta in certi dettagli più complessi.

Definire Trait con Type Associati

I type associati collegano un type segnaposto con un trait in modo che le definizioni dei metodi del trait possano usare questi segnaposto nelle loro firme. Chi implementa il trait specificherà il type concreto da usare per quella particolare implementazione. In questo modo possiamo definire un trait che usa qualche type senza dover sapere esattamente quali siano fino a quando il trait non verrà implementato.

Abbiamo detto che molte delle funzionalità avanzate di questo capitolo sono usate raramente. I type associati stanno a metà: si usano meno rispetto ad altre funzionalità spiegate nel resto del libro, ma più frequentemente di altre funzionalità in questo capitolo.

Un esempio di trait con un type associato è il trait Iterator della libreria standard. Il type associato si chiama Item e rappresenta il type dei valori su cui il type che implementa Iterator itera. La definizione del trait Iterator è mostrata nel Listato 20-13.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listato 20-13: La definizione del trait Iterator con type associato Item

Il type Item è un segnaposto, e la definizione del metodo next mostra che restituirà valori di type Option<Self::Item>. Chi implementa il trait Iterator specifica il type concreto per Item, e il metodo next ritorna un Option che contiene un valore di quel type.

I type associati potrebbero sembrare simili ai generici, visto che anche questi ultimi permettono di definire una funzione senza specificare i type. Per capirne la differenza, vediamo un’implementazione di Iterator su un type chiamato Contatore che specifica Item come u32:

File: src/lib.rs
struct Contatore {
    conteggio: u32,
}

impl Contatore {
    fn new() -> Contatore {
        Contatore { conteggio: 0 }
    }
}

impl Iterator for Contatore {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --taglio--
        if self.conteggio < 5 {
            self.conteggio += 1;
            Some(self.conteggio)
        } else {
            None
        }
    }
}

Questa sintassi ricorda i type generici. Allora perché non definire Iterator usando solo generici, come mostra il Listato 20-14?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Listato 20-14: Definizione ipotetica di Iterator usando i generici

La differenza è che usando i generici, come nel Listato 20-14, dobbiamo annotare i type in ogni implementazione; siccome potremmo anche implementare Iterator<String> for Contatore o qualsiasi altro type, potremmo avere più implementazioni di Iterator per Contatore. In altre parole, quando un trait ha un parametro generico, può essere implementato per un type più volte, cambiando i type concreti dei parametri generici ogni volta. Quando usiamo il metodo next su Contatore, dovremmo fornire annotazioni di type per indicare quale implementazione di Iterator vogliamo usare.

Con i type associati non serve annotare i type perché non possiamo implementare un trait più volte su uno stesso type. Nel Listato 20-13, con i type associati, scegliamo il type di Item una sola volta perché c’è una sola impl Iterator for Contatore. Non serve indicare che vogliamo un iteratore di u32 ogni volta che chiamiamo next su Contatore.

I type associati diventano parte del contratto del trait: chi implementa il trait deve fornire un type per sostituire il segnaposto. Spesso i type associati hanno nomi che descrivono come saranno usati, ed è buona prassi documentarli nelle API.

Usare Parametri Generici di Default e Sovrascrivere gli Operatori

Quando usiamo type generici, possiamo specificare un type concreto di default per il type generico. Questo elimina la necessità per chi implementa il trait di specificare un type concreto se il type di default va bene. Si specifica un type di default dichiarando il generico con la sintassi <TypeSegnaposto=TypeConcreto>.

Un ottimo esempio di questa tecnica è la sovrascrittura degli operatori, dove personalizzi il comportamento di un operatore (come +) in situazioni particolari.

Rust non permette di creare operatori propri o sovrascrivere operatori arbitrari. Ma puoi sovrascrivere le operazioni e i trait corrispondenti elencati in std::ops implementando i trait associati all’operatore. Per esempio, nel Listato 20-15 sovrascriviamo l’operatore + per sommare due istanze di Punto. Lo facciamo implementando il trait Add per la struct Punto.

File: src/main.rs
use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Punto {
    x: i32,
    y: i32,
}

impl Add for Punto {
    type Output = Punto;

    fn add(self, altro: Punto) -> Punto {
        Punto {
            x: self.x + altro.x,
            y: self.y + altro.y,
        }
    }
}

fn main() {
    assert_eq!(
        Punto { x: 1, y: 0 } + Punto { x: 2, y: 3 },
        Punto { x: 3, y: 3 }
    );
}
Listato 20-15: Implementazione del trait Add per sovrascrivere l’operatore + per le istanze di Punto

Il metodo add somma i valori x di due istanze Punto e i valori y di due istanze Punto per creare un nuovo Punto. Il trait Add ha un type associato chiamato Output che determina il type restituito dal metodo add.

Il type generico di default in questo codice si trova all’interno del trait Add. Ecco la sua definizione:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Questo codice dovrebbe sembrarti familiare: un trait con un metodo e un type associato. La novità è Rhs=Self: questa sintassi si chiama default type parameters ( type di default dei parametri). Il parametro generico Rhs (abbreviazione di right-hand side, lato destro) definisce il type del parametro rhs nel metodo add. Se non specifichiamo un type concreto per Rhs nell’implementazione di Add, il type di Rhs di default sarà Self, il type su cui stiamo implementando Add.

Quando abbiamo implementato Add per Punto, abbiamo usato il default per Rhs perché volevamo sommare due Punto. Passiamo ora a un esempio di implementazione del trait Add in cui vogliamo personalizzare il type Rhs invece di usare il default.

Abbiamo due struct, Millimetri e Metri, che contengono valori in unità diverse. Questo tipo di “incapsulamento sottile” attorno a un type esistente è chiamato newtype pattern, di cui parleremo più avanti nella sezione “Implementare Trait Esterni con il Modello Newtype. Vogliamo sommare valori in millimetri a valori in metri e far sì che l’implementazione di Add si occupi di fare la conversione corretta. Possiamo implementare Add per Millimetri con Metri come Rhs, come mostrato nel Listato 20-16.

File: src/lib.rs
use std::ops::Add;

struct Millimetri(u32);
struct Metri(u32);

impl Add<Metri> for Millimetri {
    type Output = Millimetri;

    fn add(self, altro: Metri) -> Millimetri {
        Millimetri(self.0 + (altro.0 * 1000))
    }
}
Listato 20-16: Implementazione del trait Add su Millimetri per sommare Millimetri con Metri

Per sommare Millimetri e Metri, specifichiamo impl Add<Metri> per impostare il valore del parametro di type Rhs invece di usare il default Self.

Userai i type di default per i parametri in due modi principali:

  1. Per estendere un type senza rompere il codice esistente
  2. Per permettere personalizzazioni in casi specifici che la maggior parte degli utenti non userà

Il trait Add della libreria standard è un esempio del secondo punto: di solito si sommano due type uguali, ma il trait Add permette di personalizzare questo comportamento. L’uso di un type di default come parametro nella definizione di Add significa che non devi specificare quel parametro extra la maggior parte delle volte, riducendo il codice ripetitivo e facilitandone l’uso.

Il primo punto è simile ma all’opposto: se vuoi aggiungere un type come parametro a un trait esistente, puoi dargli un default per permettere l’estensione della funzionalità del trait senza rompere il codice esistente.

Disambiguare Tra Metodi Con lo Stesso Nome

Nulla in Rust vieta ad un trait di avere metodi che hanno lo stesso nome di metodi in un altro trait. E Rust non ti impedisce di implementare entrambi i trait su di un type. È possibile anche definire un metodo sul type con lo stesso nome di un metodo del trait.

Quando chiami metodi con lo stesso nome devi indicare a Rust quale vuoi usare. Considera il codice nel Listato 20-17, dove sono definiti due trait, Pilota e Mago, entrambi con un metodo chiamato vola. Entrambi i trait sono implementati su un type Umano, che ha anche un metodo vola definito direttamente.

File: src/main.rs
trait Pilota {
    fn vola(&self);
}

trait Mago {
    fn vola(&self);
}

struct Umano;

impl Pilota for Umano {
    fn vola(&self) {
        println!("Qui parla il capitano.");
    }
}

impl Mago for Umano {
    fn vola(&self) {
        println!("Sali!");
    }
}

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

fn main() {}
Listato 20-17: Due trait con un metodo vola implementati sul type Umano, e un metodo vola definito direttamente su Umano

Quando chiamiamo vola su un’istanza Umano, come impostazione predefinita il compilatore chiama il metodo definito direttamente sul type, come mostrato nel Listato 20-18.

File: src/main.rs
trait Pilota {
    fn vola(&self);
}

trait Mago {
    fn vola(&self);
}

struct Umano;

impl Pilota for Umano {
    fn vola(&self) {
        println!("Qui parla il capitano.");
    }
}

impl Mago for Umano {
    fn vola(&self) {
        println!("Sali!");
    }
}

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

fn main() {
    let persona = Umano;
    persona.fly();
}
Listato 20-18: Chiamare vola su un’istanza di Umano

Questo codice stampa *sbatte furiosamente le braccia*, mostrando che Rust chiama il metodo vola definito direttamente su Umano.

Per chiamare i metodi vola dal trait Pilota o Mago serve una sintassi più esplicita per indicare quale metodo si intende. Il Listato 20-19 mostra questa sintassi.

File: src/main.rs
trait Pilota {
    fn vola(&self);
}

trait Mago {
    fn vola(&self);
}

struct Umano;

impl Pilota for Umano {
    fn vola(&self) {
        println!("Qui parla il capitano.");
    }
}

impl Mago for Umano {
    fn vola(&self) {
        println!("Sali!");
    }
}

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

fn main() {
    let persona = Umano;
    Pilota::vola(&persona);
    Mago::vola(&persona);
    persona.vola();
}
Listato 20-19: Specificare quale metodo vola di quale trait si vuole chiamare

Specificare il nome del trait prima del metodo chiarisce a Rust quale implementazione di vola vogliamo chiamare. Possiamo anche scrivere Umano::vola(&persona), che è equivalente a person.vola(), ma è più verboso se non serve disambiguare.

Questo codice stampa:

$ cargo run
   Compiling esempio-trait v0.1.0 (file:///progetti/esempio-trait)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.07s
     Running `target/debug/esempio-trait`
Qui parla il capitano.
Sali!
*sbatte furiosamente le braccia*

Poiché il metodo vola prende self come parametro, se avessimo due type che implementano entrambi un trait, Rust potrebbe inferire quale implementazione del trait usare in base al type di self.

Tuttavia, le funzioni associate che non sono metodi non hanno self. Quando ci sono più type o trait che definiscono funzioni con lo stesso nome, Rust non sa sempre quale type intendi, a meno che non si usi la sintassi completamente qualificata. Per esempio, nel Listato 20-20 creiamo un trait per un rifugio per animali che vuole chiamare tutti i cuccioli di cane Rex. Realizziamo un trait Animale con una funzione associata chiamata nomignolo. Il trait Animale è implementato per la struct Cane, sulla quale definiamo una funzione associata nomignolo.

File: src/main.rs
trait Animale {
    fn nomignolo() -> String;
}

struct Cane;

impl Cane {
    fn nomignolo() -> String {
        String::from("Rex")
    }
}

impl Animale for Cane {
    fn nomignolo() -> String {
        String::from("cucciolo")
    }
}

fn main() {
    println!("Un piccolo di cane è detto {}", Cane::nomignolo());
}
Listato 20-20: Trait con funzione associata e type con funzione associata dello stesso nome che implementa il trait

Implementiamo il codice che chiama tutti i cuccioli Rex nella funzione associata nomignolo definita direttamente in Cane. Il type Cane implementa anche il trait Animale, che descrive caratteristiche comuni a tutti gli animali. I piccoli di cane sono chiamati cuccioli, e questo è espresso nell’implementazione del trait Animale per Cane nella funzione nomignolo associata al trait Animale.

In main, chiamando Cane::nomignolo chiamiamo la funzione associata definita direttamente su Cane. Questo codice stampa:

$ cargo run
   Compiling esempio-trait v0.1.0 (file:///progetti/esempio-trait)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.10s
     Running `target/debug/esempio-trait`
Un piccolo di cane è detto Rex

Questo output non è quello voluto: vogliamo chiamare la funzione nomignolo del trait Animale implementato su Cane così che il codice stampi Un piccolo di cane è detto cucciolo. La tecnica di specificare il nome del trait come nel Listato 20-19 non aiuta; se cambiamo main con il codice del Listato 20-21, otterremo un errore di compilazione.

File: src/main.rs
trait Animale {
    fn nomignolo() -> String;
}

struct Cane;

impl Cane {
    fn nomignolo() -> String {
        String::from("Rex")
    }
}

impl Animale for Cane {
    fn nomignolo() -> String {
        String::from("cucciolo")
    }
}

fn main() {
    println!("Un piccolo di cane è detto {}", Animale::nomignolo());
}
Listato 20-21: Tentativo di chiamare la funzione nomignolo del trait Animale, ma Rust non sa quale implementazione usare

Poiché Animale::nomignolo non ha il parametro self, e potrebbero esserci altri type che implementano il trait Animale, Rust non riesce a inferire quale implementazione di Animale::nomignolo usare. Otterremo questo errore del compilatore:

$ cargo run
   Compiling esempio-trait v0.1.0 (file:///progetti/esempio-trait)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:21:47
   |
 2 |     fn nomignolo() -> String;
   |     ------------------------- `Animale::nomignolo` defined here
...
21 |     println!("Un piccolo di cane è detto {}", Animale::nomignolo());
   |                                               ^^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
21 |     println!("Un piccolo di cane è detto {}", <Cane as Animale>::nomignolo());
   |                                               ++++++++        +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `esempio-trait` (bin "esempio-trait") due to 1 previous error

Per disambiguare e indicare a Rust che vogliamo usare l’implementazione del trait Animale per Cane anziché quella per un altro type, usiamo la sintassi completamente qualificata. Il Listato 20-22 mostra come.

File: src/main.rs
trait Animale {
    fn nomignolo() -> String;
}

struct Cane;

impl Cane {
    fn nomignolo() -> String {
        String::from("Rex")
    }
}

impl Animale for Cane {
    fn nomignolo() -> String {
        String::from("cucciolo")
    }
}

fn main() {
    println!("Un piccolo di cane è detto {}", <Cane as Animale>::nomignolo());
}
Listato 20-22: Uso della sintassi completamente qualificata per chiamare la funzione nomignolo del trait Animale implementato su Cane

Forniamo un’annotazione di type tra parentesi angolari < > per dire a Rust che vogliamo chiamare il metodo nomignolo del trait Animale implementato su Cane, trattando il type Cane come un type Animale per questa chiamata. Ora il codice stampa quello che vogliamo:

$ cargo run
   Compiling esempio-trait v0.1.0 (file:///progetti/esempio-trait)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.11s
     Running `target/debug/esempio-trait`
Un piccolo di cane è detto cucciolo

In generale la sintassi completamente qualificata è:

<Tipo as Trait>::funzione(ricevente_se_metodo, prossimo_argomento,...);

Per funzioni associate che non sono metodi, non c’è un parametro self, solo la lista degli altri argomenti. Puoi usare la sintassi completamente qualificata ovunque chiami funzioni o metodi. Tuttavia, puoi omettere parti che Rust può dedurre dal contesto. Devi usarla solo quando ci sono più implementazioni con lo stesso nome e Rust ha bisogno di aiuto per distinguere quale usare.

Usare Supertrait

A volte puoi scrivere un trait che dipende da un altro trait: per un type che implementa il primo trait, richiedi che implementi anche il secondo trait. Lo fai perché la definizione del trait può usare gli elementi associati del secondo. Il trait da cui il trait che implementi dipende viene chiamato supertrait del tuo trait.

Per esempio, supponiamo di voler creare un trait StampaContorno con un metodo stampa_contorno che stampa un valore delimitato da un contorno di asterischi. Data una struct Punto che implementa il trait Display della libreria standard per mostrare (x, y), quando chiami stampa_contorno su un’istanza di Punto con x=1 e y=3, dovrebbe stampare:

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

Nell’implementazione di stampa_contorno vogliamo usare la funzionalità del trait Display. Quindi il trait StampaContorno dovrebbe funzionare solo per type che implementano anche Display. Lo specifichiamo nella definizione con StampaContorno: Display. Questo è simile ad aggiungere un vincolo di trait al trait in questione. Il Listato 20-23 mostra un implementazione del trait StampaContorno.

File: src/main.rs
use std::fmt;

trait StampaContorno: fmt::Display {
    fn stampa_contorno(&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));
    }
}

fn main() {}
Listato 20-23: Implementazione del trait StampaContorno che richiede la funzionalità di Display

Dichiarando che StampaContorno richiede il trait Display, possiamo usare la funzione to_string che è implementata automaticamente su tutti i type che implementano Display. Se provassimo a usare to_string senza specificare Display, otterremmo un errore perché il metodo to_string non sarebbe trovato per il type &Self nello scope corrente.

Vediamo cosa succede se provassimo a implementare StampaContorno su un type che non implementa Display come la struct Punto:

File: src/main.rs
use std::fmt;

trait StampaContorno: fmt::Display {
    fn stampa_contorno(&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));
    }
}

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

impl StampaContorno for Punto {}

fn main() {
    let p = Punto { x: 1, y: 3 };
    p.stampa_contorno();
}

Riceveremmo un errore che dice che Display è richiesto ma non implementato:

$ cargo run
   Compiling esempio-trait v0.1.0 (file:///progetti/esempio-trait)
error[E0277]: `Punto` doesn't implement `std::fmt::Display`
  --> src/main.rs:21:25
   |
21 | impl StampaContorno for Punto {}
   |                         ^^^^^ `Punto` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Punto`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `StampaContorno`
  --> src/main.rs:3:23
   |
3  | trait StampaContorno: fmt::Display {
   |                       ^^^^^^^^^^^^ required by this bound in `StampaContorno`

error[E0277]: `Punto` doesn't implement `std::fmt::Display`
  --> src/main.rs:26:7
   |
26 |     p.stampa_contorno();
   |       ^^^^^^^^^^^^^^^ `Punto` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Punto`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `StampaContorno::stampa_contorno`
  --> src/main.rs:3:23
   |
3  | trait StampaContorno: fmt::Display {
   |                       ^^^^^^^^^^^^ required by this bound in `StampaContorno::stampa_contorno`
4  |     fn stampa_contorno(&self) {
   |        --------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `esempio-trait` (bin "esempio-trait") due to 2 previous errors

Lo risolviamo implementando Display su Punto e soddisfiamo le necessità di StampaContorno:

File: src/main.rs
trait StampaContorno: fmt::Display {
    fn stampa_contorno(&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));
    }
}

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

impl StampaContorno for Punto {}

use std::fmt;

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

fn main() {
    let p = Punto { x: 1, y: 3 };
    p.stampa_contorno();
}

A questo punto l’implementazione di StampaContorno per Punto si compila correttamente e possiamo chiamare stampa_contorno su un’istanza di Punto per stamparlo con un contorno di asterischi.

Implementare Trait Esterni con il Modello Newtype

In “Implementare un Trait su un Type nel Capitolo 10 abbiamo parlato della orphan rule che dice che possiamo implementare un trait su un type solo se il trait o il type (o entrambi) sono locali al crate. Si può aggirare questa restrizione usando il modello newtype, che consiste nel creare un nuovo type come struct tupla. (Ne abbiamo già parlato in “Creare Type Diversi con Struct Tupla” del Capitolo 5.) La struct tupla avrà un solo campo e sarà un “incapsulamento sottile” attorno al type su cui vuoi implementare un trait. L’incapsulamento è locale al crate e puoi implementare il trait sull’incapsulatore. La parola newtype deriva dal linguaggio Haskell. Non c’è alcuna penalità in prestazioni nell’uso di questo modello, e il type dell’involucro viene eliso in fase di compilazione.

Per esempio, supponiamo di voler implementare Display su Vec<T>, cosa che la orphan rule ci impedisce perché sia il trait Display che il type Vec<T> sono definiti fuori dal nostro crate. Possiamo invece creare una struct Capsula che contiene un’istanza di Vec<T>, poi implementare Display su Capsula e usare il valore Vec<T> all’interno, come mostrato nel Listato 20-24.

File: src/main.rs
use std::fmt;

struct Capsula(Vec<String>);

impl fmt::Display for Capsula {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Capsula(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}
Listato 20-24: Creazione di un type Capsula attorno a Vec<String> per implementare Display

L’implementazione di Display usa self.0 per accedere a Vec<T> perché Capsula è una struct tupla e Vec<T> è il campo all’indice 0 della tupla. Così possiamo usare la funzionalità del trait Display su Capsula.

Lo svantaggio di questa tecnica è che Capsula è un nuovo type e non ha i metodi del valore che incapsula. Dovresti implementare manualmente tutti i metodi di Vec<T> in Capsula, in modo che i metodi deleghino a self.0, per poter usare Capsula come fosse effettivamente un Vec<T>. Se volessimo che il nuovo type abbia tutti i metodi del type interno, implementare il trait Deref su Capsula per restituire il type interno potrebbe essere una soluzione (abbiamo parlato dell’implementazione di Deref in “Trattare i Puntatori Intelligenti Come Normali Reference in Chapter 15). Se invece non vogliamo che Capsula abbia tutti i metodi del type interno, ad esempio per restringerne le funzionalità, dovremmo implementare i metodi che vogliamo manualmente.

Il modello newtype è utile anche quando non si tratta di trait. Passiamo ora a delle tecniche avanzate per interagire col sistema dei type di Rust.