Настройка: Давайте предположим, что у нас есть вызываемый Iterator
тип с параметром типа Element
:
interface Iterator<Element> {}
Тогда у нас есть интерфейс, Iterable
который имеет один метод, который будет возвращать Iterator
.
// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
getIterator(): T
}
Проблема с Iterator
универсальностью заключается в том, что мы должны предоставить ему аргументы типа.
Одна из идей для решения этой проблемы состоит в том, чтобы «вывести» тип итератора. Следующий псевдокод выражает идею, что существует переменная типа, Element
которая выводится как аргумент типа Iterator
:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
И тогда мы используем это где-то так:
class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}
Это определение Iterable
Безразлично»т использовать Element
где - либо еще в его определении , но мой реальный вариант использования делает. Определенные функции, которые используют, Iterable
также должны иметь возможность ограничивать свои параметры, чтобы принимать Iterable
s, которые возвращают только определенные виды итераторов, такие как двунаправленный итератор, поэтому возвращаемый итератор параметризован, а не только тип элемента.
Вопросов:
- Есть ли установленное имя для этих предполагаемых переменных типа? А как насчет техники в целом? Незнание конкретной номенклатуры затруднило поиск таких примеров в дикой природе или изучение специфических особенностей языка.
- Не все языки с генериками имеют эту технику; есть ли названия для подобных методов в этих языках?
источник
Ответы:
Я не знаю, есть ли конкретный термин для этой проблемы, но есть три основных класса решений:
И, конечно, стандартное решение: продолжайте указывать все эти параметры.
Избегайте конкретных типов.
Вы определили
Iterable
интерфейс как:Это дает пользователям интерфейса максимальную мощность, потому что они получают точный конкретный тип
T
итератора. Это также позволяет компилятору применять больше оптимизаций, таких как встраивание.Однако, если
Iterator<E>
это динамически отправляемый интерфейс, то знание конкретного типа не требуется. Это, например, решение, которое использует Java. Интерфейс был бы тогда написан как:Интересной вариацией этого является
impl Trait
синтаксис Rust, который позволяет вам объявлять функцию с абстрактным типом возврата, но зная, что конкретный тип будет известен на сайте вызова (что позволяет оптимизировать). Это ведет себя подобно неявному параметру типа.Разрешить параметры типа заполнителя.
Iterable
Интерфейс не нужно знать о типе элемента, так что можно было бы написать это как:Где
T: Iterator<_>
выражает ограничение «T - любой итератор, независимо от типа элемента». Более строго, мы можем выразить это как: «существует некоторый тип,Element
которыйT
естьIterator<Element>
», без необходимости знать какой-либо конкретный тип дляElement
. Это означает, что выражение типаIterator<_>
не описывает фактический тип и может использоваться только как ограничение типа.Используйте семейства типов / связанные типы.
Например, в C ++ тип может иметь члены типа. Это обычно используется в стандартной библиотеке, например
std::vector::value_type
. Это на самом деле не решает проблему параметров типа во всех сценариях, но, поскольку тип может ссылаться на другие типы, один параметр типа может описывать целое семейство связанных типов.Давайте определим:
Затем:
Это выглядит очень гибко, но обратите внимание, что это может усложнить выражение ограничений типа. Например, как написано
Iterable
, не применяется ни один тип элемента итератора, и мы могли быinterface Iterator<T>
вместо этого объявить . И вы сейчас имеете дело с довольно сложным исчислением типов. Очень легко случайно сделать такую систему типов неразрешимой (или, может быть, она уже есть?).Обратите внимание, что связанные типы могут быть очень удобны в качестве значений по умолчанию для параметров типа. Например, если предположить, что
Iterable
интерфейсу нужен отдельный параметр типа для типа элемента, который обычно, но не всегда совпадает с типом элемента итератора, и что у нас есть параметры типа заполнителя, можно сказать:Однако это всего лишь функция эргономики языка, которая не делает язык более мощным.
Системы типов сложны, поэтому стоит взглянуть на то, что работает, а что нет на других языках.
Например, подумайте о прочтении главы « Расширенные черты» в Rust Book, в которой обсуждаются связанные типы. Но учтите, что некоторые моменты в пользу связанных типов вместо общих применяются только там, потому что язык не имеет подтипов, и каждая черта может быть реализована не более одного раза для каждого типа. Т.е. черты Rust не являются Java-подобными интерфейсами.
Другие интересные системы типов включают в себя Haskell с различными языковыми расширениями. Модули / функторы OCaml являются сравнительно простой версией семейств типов, без непосредственного смешения их с объектами или параметризованными типами. Java отличается ограничениями в своей системе типов, например, обобщениями с удалением типов и отсутствием обобщений над типами значений. C # очень похож на Java, но ему удается избежать большинства этих ограничений за счет увеличения сложности реализации. Scala пытается интегрировать дженерики в стиле C # с классами типов в стиле Haskell поверх платформы Java. Обманчиво простые шаблоны C ++ хорошо изучены, но не похожи на большинство реализаций дженериков.
Также стоит взглянуть на стандартные библиотеки этих языков (особенно на стандартные коллекции библиотек, такие как списки или хеш-таблицы), чтобы увидеть, какие шаблоны обычно используются. Например, C ++ имеет сложную систему различных возможностей итератора, а Scala кодирует возможности детального сбора в качестве признаков. Стандартные интерфейсы библиотеки Java иногда не работают, например
Iterator#remove()
, но могут использовать вложенные классы как своего рода связанный тип (напримерMap.Entry
).источник