Wann ist es angebracht, einen zugeordneten Typ gegenüber einem generischen Typ zu verwenden?

106

In dieser Frage trat ein Problem auf, das gelöst werden konnte, indem ein Versuch, einen generischen Typparameter zu verwenden, in einen zugeordneten Typ geändert wurde. Dies führte zu der Frage "Warum ist ein zugehöriger Typ hier besser geeignet?", Was mich dazu brachte, mehr zu wissen.

Der RFC, der zugehörige Typen eingeführt hat, lautet:

Dieser RFC verdeutlicht die Übereinstimmung von Merkmalen durch:

  • Behandeln aller Merkmalstypparameter als Eingabetypen und
  • Bereitstellen zugeordneter Typen, bei denen es sich um Ausgabetypen handelt .

Der RFC verwendet eine Diagrammstruktur als motivierendes Beispiel, und dies wird auch in der Dokumentation verwendet , aber ich gebe zu, dass ich die Vorteile der zugehörigen Typversion gegenüber der typparametrierten Version nicht vollständig einschätze. Die Hauptsache ist, dass sich die distanceMethode nicht um das kümmern mussEdge Typ . Das ist schön, scheint aber ein wenig oberflächlich zu sein, um überhaupt assoziierte Typen zu haben.

Ich habe festgestellt, dass zugehörige Typen in der Praxis ziemlich intuitiv zu verwenden sind, aber ich habe Schwierigkeiten, zu entscheiden, wo und wann ich sie in meiner eigenen API verwenden soll.

Wann sollte ich beim Schreiben von Code einen zugeordneten Typ anstelle eines generischen Typparameters auswählen und wann sollte ich das Gegenteil tun?

Shepmaster
quelle

Antworten:

74

Dies wird jetzt in der zweiten Ausgabe von The Rust Programming Language angesprochen . Lassen Sie uns jedoch zusätzlich ein wenig eintauchen.

Beginnen wir mit einem einfacheren Beispiel.

Wann ist es angebracht, eine Merkmalsmethode anzuwenden?

Es gibt mehrere Möglichkeiten, eine späte Bindung bereitzustellen :

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

Oder:

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)
    }
}

In beiden obigen Auszügen kann der Benutzer ohne Berücksichtigung einer Implementierungs- / Leistungsstrategie auf dynamische Weise angeben, wie hello_world sich verhalten soll.

Der einzige Unterschied (semantisch) besteht darin, dass die traitImplementierung garantiert, dass für einen bestimmten Typ, der das Timplementiert trait, hello_worlddas immer das gleiche Verhalten aufweist, während dasstruct Implementierung ein unterschiedliches Verhalten pro Instanz zulässt.

Ob die Verwendung einer Methode angemessen ist oder nicht, hängt vom Anwendungsfall ab!

Wann ist es angebracht, einen zugeordneten Typ zu verwenden?

Ähnlich wie bei den traitobigen Methoden ist ein zugeordneter Typ eine Form der späten Bindung (obwohl sie bei der Kompilierung auftritt), sodass der Benutzer von traitfür eine bestimmte Instanz angeben kann, welcher Typ ersetzt werden soll. Es ist nicht der einzige Weg (also die Frage):

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

Oder:

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

Entsprechen der späten Bindung der oben genannten Methoden:

  • Der erste erzwingt, dass für eine bestimmte SelfPerson eine einzelne Returnzugeordnet ist
  • die zweite, sondern ermöglicht die Umsetzung MyTraitfür Selffür mehrereReturn

Welche Form besser geeignet ist, hängt davon ab, ob es sinnvoll ist, die Einheit durchzusetzen oder nicht. Beispielsweise:

  • Deref verwendet einen zugeordneten Typ, da der Compiler ohne Eindeutigkeit während der Inferenz verrückt werden würde
  • Add verwendet einen zugeordneten Typ, da der Autor der Ansicht war, dass es angesichts der beiden Argumente einen logischen Rückgabetyp geben würde

Wie Sie sehen können, Derefist der Fall von zwar ein offensichtlicher Anwendungsfall (technische Einschränkung), Addaber weniger eindeutig: Vielleicht wäre es sinnvoll i32 + i32, entweder i32oder Complex<i32>abhängig vom Kontext nachzugeben ? Der Autor übte jedoch sein Urteil aus und entschied, dass eine Überladung des Rückgabetyps für Ergänzungen nicht erforderlich sei.

Meine persönliche Haltung ist, dass es keine richtige Antwort gibt. Über das Unicity-Argument hinaus möchte ich jedoch erwähnen, dass zugeordnete Typen die Verwendung des Merkmals erleichtern, da sie die Anzahl der Parameter verringern, die angegeben werden müssen. Falls die Vorteile der Flexibilität der Verwendung eines regulären Merkmalsparameters nicht offensichtlich sind, I. Schlagen Sie vor, mit einem zugeordneten Typ zu beginnen.

Matthieu M.
quelle
4
Lassen Sie mich versuchen, ein bisschen zu vereinfachen: trait/struct MyTrait/MyStructerlaubt genau eine impl MyTrait foroder impl MyStruct. trait MyTrait<Return>erlaubt mehrere impls, weil es generisch ist. Returnkann jeder Typ sein. Generische Strukturen sind gleich.
Paul-Sebastian Manole
2
Ich finde Ihre Antwort viel einfacher zu verstehen als die in "The Rust Programming Language"
drojf
"Der erste erzwingt, dass für ein bestimmtes Selbst eine einzige Rückkehr verbunden ist". Dies ist im unmittelbaren Sinne wahr, aber man könnte diese Einschränkung natürlich umgehen, indem man sie mit einem generischen Merkmal unterklassifiziert. Vielleicht kann
Einigkeit
36

Zugehörige Typen sind ein Gruppierungsmechanismus. Sie sollten daher verwendet werden, wenn es sinnvoll ist, Typen zu gruppieren.

Das Graphin der Dokumentation eingeführte Merkmal ist ein Beispiel dafür. Sie möchten Graph, dass a generisch ist, aber sobald Sie eine bestimmte Art von haben Graph, möchten Sie nicht, dass die Nodeoder EdgeTypen mehr variieren. Ein bestimmter GraphBenutzer möchte diese Typen nicht innerhalb einer einzelnen Implementierung variieren und möchte, dass sie immer gleich sind. Sie sind zusammen gruppiert, oder man könnte sogar sagen assoziiert .

Steve Klabnik
quelle
4
Ich brauchte einige Zeit, um zu verstehen. Für mich sieht es eher so aus, als würden mehrere Typen gleichzeitig definiert: Die Kante und der Knoten ergeben im Diagramm keinen Sinn.
Tafia