In deze vraagdeed zich een probleem voor dat kon worden opgelost door een poging om een generiek type parameter te gebruiken, te veranderen in een bijbehorende soort. Dat leidde tot de vraag “Waarom is een bijbehorend type hier meer op zijn plaats?”, waardoor ik meer wilde weten.
De RFC die bijbehorende typen introduceerdezegt:
Deze RFC verduidelijkt het matchen van eigenschappen door:
- Alle parameters van het kenmerktype behandelen als invoertypes, en
- Geassocieerde typen bieden, dit zijn uitvoertypen.
De RFC gebruikt een grafiekstructuur als motiverend voorbeeld, en deze wordt ook gebruikt in de documentatie, maar ik geef toe dat ik de voordelen van de bijbehorende typeversie ten opzichte van het type niet volledig waardeer -geparametriseerde versie. Het belangrijkste is dat de distance
-methode zich geen zorgen hoeft te maken over het Edge
-type. Dit is leuk, maar lijkt een beetje oppervlakkige reden om überhaupt geassocieerde typen te hebben.
Ik heb gemerkt dat de bijbehorende typen in de praktijk vrij intuïtief zijn om te gebruiken, maar ik vind het moeilijk om te beslissen waar en wanneer ik ze in mijn eigen API moet gebruiken.
Wanneer moet ik bij het schrijven van code een bijbehorend type boven een generieke typeparameter kiezen, en wanneer moet ik het tegenovergestelde doen?
Antwoord 1, autoriteit 100%
Hier wordt nu op ingegaan in de tweede editie van The Rust Programming Language. Laten we er echter nog een beetje in duiken.
Laten we beginnen met een eenvoudiger voorbeeld.
Wanneer is het gepast om een eigenschapsmethode te gebruiken?
Er zijn meerdere manieren om late bindingte bieden:
trait MyTrait {
fn hello_word(&self) -> String;
}
Of:
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)
}
}
Afgezien van een implementatie-/prestatiestrategie, kunnen beide bovenstaande fragmenten de gebruiker op een dynamische manier specificeren hoe hello_world
zich moet gedragen.
Het enige verschil (semantisch) is dat de trait
implementatie garandeert dat voor een bepaald type T
implementatie van de trait
, hello_world
zal altijd hetzelfde gedrag vertonen, terwijl de implementatie van struct
een ander gedrag per instantie mogelijk maakt.
Of het gebruik van een methode geschikt is of niet, hangt af van de usecase!
Wanneer is het gepast om een bijbehorend type te gebruiken?
Net als bij de trait
-methoden hierboven, is een geassocieerd type een vorm van late binding (hoewel het voorkomt bij compilatie), waardoor de gebruiker van de trait
kan specificeren voor een bepaald exemplaar welk type te vervangen. Het is niet de enige manier (dus de vraag):
trait MyTrait {
type Return;
fn hello_world(&self) -> Self::Return;
}
Of:
trait MyTrait<Return> {
fn hello_world(&Self) -> Return;
}
Zijn gelijk aan de late binding van bovenstaande methoden:
- de eerste dwingt af dat er voor een gegeven
Self
een enkeleReturn
is gekoppeld - de tweede maakt het in plaats daarvan mogelijk om
MyTrait
forSelf
te implementeren voor meerdereReturn
Welke vorm het meest geschikt is, hangt af van of het zinvol is om uniciteit af te dwingen of niet. Bijvoorbeeld:
Deref
gebruikt een bijbehorend type omdat zonder uniciteit de compiler gek zou worden tijdens gevolgtrekkingAdd
gebruikt een bijbehorend type omdat de auteur dacht dat gezien de twee argumenten er een logisch retourtype zou zijn
Zoals je kunt zien, is Deref
een voor de hand liggende usecase (technische beperking), maar het geval van Add
is minder duidelijk: misschien is het logisch voor i32 + i32
om ofwel i32
of Complex<i32>
op te leveren, afhankelijk van de context? Desalniettemin oefende de auteur zijn oordeel uit en besloot dat het niet nodig was om het retourtype voor toevoegingen te overladen.
Mijn persoonlijke standpunt is dat er geen juist antwoord is. Maar afgezien van het uniciteitsargument, zou ik willen vermelden dat geassocieerde typen het gebruik van de eigenschap gemakkelijker maken omdat ze het aantal parameters dat moet worden gespecificeerd verminderen, dus in het geval dat de voordelen van de flexibiliteit van het gebruik van een reguliere eigenschapparameter niet duidelijk zijn, ik stel voor te beginnen met een bijbehorend type.
Antwoord 2, autoriteit 43%
Geassocieerde typen zijn een groeperingsmechanisme, dus ze moeten worden gebruikt wanneer het zinvol is om typen samen te groeperen.
De eigenschap Graph
die in de documentatie is geïntroduceerd, is hier een voorbeeld van. U wilt dat een Graph
generiek is, maar als u eenmaal een specifiek soort Graph
heeft, wilt u niet dat de Node
of Edge
-typen om niet meer te variëren. Een bepaalde Graph
zal deze typen niet binnen een enkele implementatie willen variëren, en in feite wil dat ze altijd hetzelfde zijn. Ze zijn gegroepeerd, of men zou zelfs kunnen zeggen geassocieerd.
Antwoord 3
Geassocieerde typen kunnen worden gebruikt om de compiler te vertellen dat “deze twee typen tussen deze twee implementaties hetzelfde zijn”. Hier is een voorbeeld met dubbele verzending dat compileert en bijna hetzelfde is als hoe de standaardbibliotheek iterator relateert aan somtypen:
trait MySum {
type Item;
fn sum<I>(iter: I)
where
I: MyIter<Item = Self::Item>;
}
trait MyIter {
type Item;
fn next(&self) {}
fn sum<S>(self)
where
S: MySum<Item = Self::Item>;
}
struct MyU32;
impl MySum for MyU32 {
type Item = MyU32;
fn sum<I>(iter: I)
where
I: MyIter<Item = Self::Item>,
{
iter.next()
}
}
struct MyVec;
impl MyIter for MyVec {
type Item = MyU32;
fn sum<S>(self)
where
S: MySum<Item = Self::Item>,
{
S::sum::<Self>(self)
}
}
fn main() {}
Ook https://blog.thomasheartman.com/posts /on-generics-and-associated-typesheeft hier ook goede informatie over:
Kortom, gebruik generieke geneesmiddelen wanneer u
A
wilt typen om een eigenschap een willekeurig aantal keren te kunnen implementeren voor verschillende typeparameters, zoals in het geval van de eigenschap Van.
Gebruik gekoppelde typen als het logisch is dat een type de eigenschap maar één keer implementeert, zoals bij Iterator en Deref.