Quando è appropriato utilizzare un tipo associato rispetto a un tipo generico?


109

In questa domanda è emerso un problema che potrebbe essere risolto modificando un tentativo di utilizzare un parametro di tipo generico in un tipo associato. Ciò ha portato alla domanda "Perché un tipo associato è più appropriato qui?", Che mi ha fatto desiderare di saperne di più.

La RFC che ha introdotto i tipi associati dice:

Questa RFC chiarisce la corrispondenza dei tratti tramite:

  • Trattare tutti i parametri del tipo di tratto come tipi di input e
  • Fornire i tipi associati, che sono tipi di output .

L'RFC utilizza una struttura a grafo come esempio motivante, e questo viene utilizzato anche nella documentazione , ma ammetto di non apprezzare appieno i vantaggi della versione del tipo associata rispetto alla versione con parametri di tipo. La cosa principale è che il distancemetodo non ha bisogno di preoccuparsi del Edgetipo. Questo è carino, ma sembra un motivo un po 'superficiale per avere tipi associati.

Ho scoperto che i tipi associati sono piuttosto intuitivi da usare nella pratica, ma mi trovo in difficoltà nel decidere dove e quando usarli nella mia API.

Quando si scrive codice, quando devo scegliere un tipo associato su un parametro di tipo generico e quando devo fare il contrario?

Risposte:


76

Questo è ora toccato nella seconda edizione di The Rust Programming Language . Tuttavia, tuffiamoci un po 'in più.

Cominciamo con un esempio più semplice.

Quando è appropriato utilizzare un metodo dei tratti?

Esistono diversi modi per fornire l' associazione tardiva :

trait MyTrait {
    fn hello_word(&self) -> String;
}

O:

struct MyTrait<T> {
    t: T,
    hello_world: fn(&T) -> String,
}

impl<T> MyTrait<T> {
    fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>;

    fn hello_world(&self) -> String {
        (self.hello_world)(self.t)
    }
}

Ignorando qualsiasi strategia di implementazione / prestazione, entrambi gli estratti sopra consentono all'utente di specificare in modo dinamico come hello_worlddeve comportarsi.

L'unica differenza (semanticamente) è che l' traitimplementazione garantisce che per un dato tipo che Timplementa il trait, hello_worldavrà sempre lo stesso comportamento mentre l' structimplementazione consente di avere un comportamento diverso per istanza.

Se l'utilizzo di un metodo è appropriato o meno dipende dal caso d'uso!

Quando è opportuno utilizzare un tipo associato?

Analogamente ai traitmetodi precedenti, un tipo associato è una forma di associazione tardiva (sebbene si verifichi alla compilazione), che consente all'utente di traitspecificare per una data istanza quale tipo sostituire. Non è l'unico modo (quindi la domanda):

trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

O:

trait MyTrait<Return> {
    fn hello_world(&Self) -> Return;
}

Sono equivalenti al binding tardivo dei metodi di cui sopra:

  • il primo impone che per un dato Selfci sia un solo Returnassociato
  • il secondo, invece, consente di attuazione MyTraitper Selfper moltepliciReturn

Quale forma sia più appropriata dipende dal fatto che abbia senso rafforzare o meno l'unicità. Per esempio:

  • Deref usa un tipo associato perché senza unicità il compilatore impazzirebbe durante l'inferenza
  • Add usa un tipo associato perché il suo autore pensava che dati i due argomenti ci sarebbe stato un tipo di ritorno logico

Come puoi vedere, mentre Derefè un caso d'uso ovvio (vincolo tecnico), il caso di Addè meno netto: forse avrebbe senso i32 + i32che cedesse i32o a Complex<i32>seconda del contesto? Tuttavia, l'autore ha esercitato il proprio giudizio e ha deciso che non era necessario sovraccaricare il tipo di ritorno per le aggiunte.

La mia posizione personale è che non esiste una risposta giusta. Tuttavia, al di là dell'argomento dell'unicità, vorrei menzionare che i tipi associati facilitano l'uso del tratto poiché diminuiscono il numero di parametri che devono essere specificati, quindi nel caso in cui i vantaggi della flessibilità dell'utilizzo di un parametro di tratto regolare non siano ovvi, io suggerire di iniziare con un tipo associato.


4
Vorrei provare a semplificare un po ': trait/struct MyTrait/MyStructconsente esattamente uno impl MyTrait foro impl MyStruct. trait MyTrait<Return>consente più messaggi di posta implelettronica perché è generico. Returnpuò essere di qualsiasi tipo. Gli struct generici sono gli stessi.
Paul-Sebastian Manole

2
Trovo la tua risposta molto più facile da capire rispetto a quella in "The Rust Programming Language"
drojf

"il primo impone che per un dato Sé ci sia un unico Ritorno associato". Questo è vero nel senso immediato, ma si potrebbe ovviamente aggirare questa restrizione creando sottoclassi con un tratto generico. Forse l'unicità può essere solo un suggerimento e non applicato
joel

37

I tipi associati sono un meccanismo di raggruppamento , quindi dovrebbero essere usati quando ha senso raggruppare i tipi insieme.

Il Graphtratto introdotto nella documentazione ne è un esempio. Vuoi che un Graphsia generico, ma una volta che hai un tipo specifico di Graph, non vuoi che i tipi Nodeo Edgecambino più. Un particolare Graphnon vorrà variare quei tipi all'interno di una singola implementazione e, in effetti, vuole che siano sempre gli stessi. Sono raggruppati insieme, o si potrebbe anche dire associati .


5
Mi ci è voluto del tempo per capire. A me sembra più come definire diversi tipi contemporaneamente: Edge e Node non hanno senso dal grafico.
tafia
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.