Ich weiß nicht, ob es einen bestimmten Begriff für dieses Problem gibt, aber es gibt drei allgemeine Klassen von Lösungen:
- Vermeiden Sie konkrete Typen zugunsten eines dynamischen Versands
- Platzhaltertypparameter in Typeinschränkungen zulassen
- Vermeiden Sie Typparameter, indem Sie zugehörige Typen / Typfamilien verwenden
Und natürlich die Standardlösung: Schreiben Sie all diese Parameter weiter aus.
Vermeiden Sie Betontypen.
Sie haben eine Iterable
Schnittstelle definiert als:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Dies gibt Benutzern der Schnittstelle maximale Leistung, da sie den genauen konkreten Typ T
des Iterators erhalten. Auf diese Weise kann ein Compiler auch weitere Optimierungen wie Inlining anwenden.
Wenn Iterator<E>
es sich jedoch um eine dynamisch versendete Schnittstelle handelt, ist es nicht erforderlich, den konkreten Typ zu kennen. Dies ist zB die Lösung, die Java verwendet. Die Schnittstelle würde dann wie folgt geschrieben:
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
Eine interessante Variante davon ist die impl Trait
Syntax von Rust, mit der Sie die Funktion mit einem abstrakten Rückgabetyp deklarieren können, aber wissen, dass der konkrete Typ an der Aufrufstelle bekannt ist (wodurch Optimierungen möglich sind). Dies verhält sich ähnlich wie bei einem impliziten Typparameter.
Platzhaltertypparameter zulassen.
Die Iterable
Schnittstelle muss den Elementtyp nicht kennen, daher kann dies möglicherweise wie folgt geschrieben werden:
interface Iterable<T: Iterator<_>> {
getIterator(): T
}
Wobei T: Iterator<_>
die Einschränkung "T ist ein beliebiger Iterator, unabhängig vom Elementtyp" ausgedrückt wird. Streng genommen können wir dies so ausdrücken: „Es gibt einen Typ Element
, der ein T
ist Iterator<Element>
“, ohne dass wir einen konkreten Typ dafür kennen müssen Element
. Dies bedeutet, dass der Typausdruck Iterator<_>
keinen tatsächlichen Typ beschreibt und nur als Typeinschränkung verwendet werden kann.
Verwenden Sie Typfamilien / zugehörige Typen.
In C ++ kann ein Typ beispielsweise Typmitglieder haben. Dies wird üblicherweise in der gesamten Standardbibliothek verwendet, z std::vector::value_type
. Dies löst das Problem mit den Typparametern nicht in allen Szenarien. Da sich ein Typ jedoch möglicherweise auf andere Typen bezieht, kann ein einzelner Typparameter eine ganze Familie verwandter Typen beschreiben.
Definieren wir:
interface Iterator {
type ElementType
fn next(): ElementType
}
interface Iterable {
type IteratorType: Iterator
fn getIterator(): IteratorType
}
Dann:
class Vec<Element> implement Iterable {
type IteratorType = VecIterator<Element>
fn getIterator(): IteratorType { ... }
}
class VecIterator<T> implements Iterator {
type ElementType = T
fn next(): ElementType { ... }
}
Dies sieht sehr flexibel aus. Beachten Sie jedoch, dass dies das Ausdrücken von Typeinschränkungen erschweren kann. Zum Beispiel Iterable
erzwingt, wie geschrieben , keinen Iteratorelementtyp, und wir möchten möglicherweise interface Iterator<T>
stattdessen deklarieren . Und Sie haben es jetzt mit einem ziemlich komplexen Typkalkül zu tun. Es ist sehr einfach, ein solches Typsystem versehentlich unentscheidbar zu machen (oder vielleicht schon?).
Beachten Sie, dass zugeordnete Typen als Standardeinstellungen für Typparameter sehr praktisch sein können. Unter der Annahme, dass die Iterable
Schnittstelle einen separaten Typparameter für den Elementtyp benötigt, der normalerweise, aber nicht immer mit dem Iteratorelementtyp identisch ist, und dass wir Platzhaltertypparameter haben, kann möglicherweise Folgendes gesagt werden:
interface Iterable<T: Iterator<_>, Element = T::Element> {
...
}
Dies ist jedoch nur eine Sprachergonomiefunktion und macht die Sprache nicht leistungsfähiger.
Typsysteme sind schwierig, daher ist es gut, einen Blick darauf zu werfen, was in anderen Sprachen funktioniert und was nicht.
Lesen Sie beispielsweise das Kapitel Erweiterte Eigenschaften im Rostbuch, in dem die zugehörigen Typen erläutert werden. Beachten Sie jedoch, dass einige Punkte zugunsten zugeordneter Typen anstelle von Generika nur dort gelten, da die Sprache keine Untertypisierung aufweist und jedes Merkmal höchstens einmal pro Typ implementiert werden kann. Dh Rust-Merkmale sind keine Java-ähnlichen Schnittstellen.
Andere interessante Typsysteme umfassen Haskell mit verschiedenen Spracherweiterungen. OCaml- Module / Funktoren sind eine vergleichsweise einfache Version von Typfamilien, ohne sie direkt mit Objekten oder parametrisierten Typen zu vermischen. Java zeichnet sich durch Einschränkungen in seinem Typsystem aus, z. B. Generika mit Typlöschung und keine Generika über Werttypen. C # ist sehr Java-ähnlich, kann jedoch die meisten dieser Einschränkungen auf Kosten einer höheren Komplexität der Implementierung vermeiden. Scala versucht, Generika im C # -Stil in Typklassen im Haskell-Stil auf der Java-Plattform zu integrieren. Die täuschend einfachen Vorlagen von C ++ sind gut untersucht, unterscheiden sich jedoch von den meisten generischen Implementierungen.
Es lohnt sich auch, die Standardbibliotheken dieser Sprachen (insbesondere Standardbibliothekssammlungen wie Listen oder Hash-Tabellen) zu betrachten, um festzustellen, welche Muster häufig verwendet werden. Beispielsweise verfügt C ++ über ein komplexes System mit verschiedenen Iteratorfunktionen, und Scala codiert feinkörnige Erfassungsfunktionen als Merkmale. Die Java-Standardbibliotheksschnittstellen sind manchmal nicht funktionsfähig, Iterator#remove()
können jedoch verschachtelte Klassen als eine Art zugeordneten Typ verwenden (z Map.Entry
. B. ).