Wanneer is het gepast om een ​​geassocieerd type te gebruiken in plaats van een generiek type?

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_worldzich moet gedragen.

Het enige verschil (semantisch) is dat de traitimplementatie garandeert dat voor een bepaald type Timplementatie van de trait, hello_worldzal altijd hetzelfde gedrag vertonen, terwijl de implementatie van structeen 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 traitkan 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 Selfeen enkele Returnis gekoppeld
  • de tweede maakt het in plaats daarvan mogelijk om MyTraitfor Selfte implementeren voor meerdere Return

Welke vorm het meest geschikt is, hangt af van of het zinvol is om uniciteit af te dwingen of niet. Bijvoorbeeld:

  • Derefgebruikt een bijbehorend type omdat zonder uniciteit de compiler gek zou worden tijdens gevolgtrekking
  • Addgebruikt een bijbehorend type omdat de auteur dacht dat gezien de twee argumenten er een logisch retourtype zou zijn

Zoals je kunt zien, is Derefeen voor de hand liggende usecase (technische beperking), maar het geval van Addis minder duidelijk: misschien is het logisch voor i32 + i32om ofwel i32of 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 Graphdie in de documentatie is geïntroduceerd, is hier een voorbeeld van. U wilt dat een Graphgeneriek is, maar als u eenmaal een specifiek soort Graphheeft, wilt u niet dat de Nodeof Edge-typen om niet meer te variëren. Een bepaalde Graphzal 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 Awilt 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.

Other episodes