I tipi associati sono una parte potente del sistema di tipi di Rust. Sono
correlati all'idea di una ‘famiglia di tipi’, in altre parole, raggruppando
insieme più tipi. Questa descrizione è un po' astratta, e quindi tuffiamoci
subito in un esempio. Se si vuole scrivere un tratto Grafo
, ci sono due tipi
sui quali essere generici: il tipo dei nodi e il tipo degli archi. Perciò
si potrebbe scrivere un tratto, Grafo<N, A>
, così:
trait Grafo<N, A> { fn ha_arco(&self, &N, &N) -> bool; fn archi(&self, &N) -> Vec<A>; // ecc }
Mentre questo in qualche modo funziona, finisce per essere goffo. Per esempio,
ogni funione che vuole prendere un Grafo
come parametro adesso ha bisogno
anche di essere generica sui tipi N
odo e A
rco:
fn distanza<N, A, G: Grafo<N, A>>(grafo: &G, inizio: &N, fine: &N) -> u32 { ... }
Il nostro calcolo di distanza funziona indipendentemente dal nostro tipo
Arco
, e quindi citare la A
in questa firma è una distrazione.
Ciò che vogliamo realmente dire è che un certo tipo A
rco e un certo tipo
N
odo si mettono insieme per formare ogni tipo di Grafo
. Lo possiamo fare
con i tipi associati:
trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; // ecc }
Adesso, i nostri clienti possono usare tutta l'astrazione di un dato Graph
:
fn distanza<G: Graph>(grafo: &G, inizio: &G::N, fine: &G::N) -> u32 { ... }
Qui non c'è bisogno di trattare con il tipo A
rco!
Analizziamolo in maggiore dettaglio.
Costruiamo quel tratto Grafo
. Ecco la definizione:
trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; }
Abbastanza semplice. I tipi associati usano la parola-chiave type
, e vanno
dentro il corpo del tratto, insieme alle funzioni.
Queste dichiarazioni type
possono avere tutte le cose che hanno le funzioni.
Per esempio, se volessimo che il nostro tipo N
implementi Display
, così da
poter stampare i nodi, potremmo fare così:
use std::fmt; trait Grafo { type N: fmt::Display; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; }
Proprio come ogni tratto, i tratti che usano tipi associati usano
la parola-chiave impl
per fornire implementazioni. Ecco una semplice
implementazione di Grafo:
struct Nodo; struct Arco; struct IlMioGrafo; impl Grafo for IlMioGrafo { type N = Nodo; type A = Arco; fn ha_arco(&self, n1: &Nodo, n2: &Nodo) -> bool { true } fn archi(&self, n: &Nodo) -> Vec<Arco> { Vec::new() } }
Questa implementazione sciocca restituisce sempre true
e un Vec<Arco>
vuoto, ma dà un'idea di come implementare questo tipo di cose.
Dapprima ci servono tre
struct
, una per il grafo, una per il nodo, e una per l'arco. Se avesse
più senso usare un altro tipo, quello funzionerebbe altrettanto, qui useremo
le struct
per tutti e tre.
Poi c'è la riga impl
, che è un'implementazione come per qualunque
altro tratto.
Da qui, usiamo =
per definire i nostri tipi associati. Il nome usato
dal tratto va sulla sinistra dell'=
, e il tipo concreto per cui stiamo
implementando questo va sulla destra. Infine, usiamo i tipi concreti
nelle nostre dichiarazioni di funzione.
C'è un altro pezzo di sintassi di cui dovremmo parlare: gli oggetti-tratto. Se si prova a creare un oggetto-tratto da un tratto con un tipo associato, così:
fn main() { trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; } struct Nodo; struct Arco; struct IlMioGrafo; impl Grafo for IlMioGrafo { type N = Nodo; type A = Arco; fn ha_arco(&self, n1: &Nodo, n2: &Nodo) -> bool { true } fn archi(&self, n: &Nodo) -> Vec<Arco> { Vec::new() } } let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo>; }let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo>;
Otterremo due errori:
error: the value of the associated type `A` (from the trait `main::Grafo`) must
be specified [E0191]
let ogg = Box::new(grafo) as Box<Grafo>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Grafo`) must be specified [E0191]
let ogg = Box::new(grafo) as Box<Grafo>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Non possiamo creare un oggetto-tratto così, perché non conosciamo i tipi associati. Invece, possiamo scrivere così:
fn main() { trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; } struct Nodo; struct Arco; struct IlMioGrafo; impl Grafo for IlMioGrafo { type N = Nodo; type A = Arco; fn ha_arco(&self, n1: &Nodo, n2: &Nodo) -> bool { true } fn archi(&self, n: &Nodo) -> Vec<Arco> { Vec::new() } } let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo<N=Nodo, A=Arco>>; }let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo<N=Nodo, A=Arco>>;
La sintassi N=Nodo
ci permette di fornire un tipo concreto, Nodo
, per
il parametro di tipo N
. Lo stesso con A=Arco
. Se non fornissimo questo
vincolo, non potremmo essere sicuri di quale impl
corrisponda a questo
oggetto-tratto.