В любом языке программирования, который сочетает полиморфизм подтипа (ориентация объекта) с параметрическим полиморфизмом (обобщение), возникает вопрос о дисперсии . Предположим, у меня есть список строк, типа List<String> . Могу ли я передать это функции, которая принимает List<Object> ? Давайте начнем с этого определения:
|
1
2
3
4
5
|
interface List<T> { void add(T element); Iterator<T> iterator(); ...} |
Сломанная ковариация
Интуитивно, мы можем сначала подумать, что это должно быть разрешено. Это выглядит хорошо:
|
1
2
3
4
5
|
void iterate(List<Object> list) { Iterator<Object> it = list.iterator(); ...}iterate(ArrayList<String>()); |
Действительно, некоторые языки, включая Eiffel и Dart, принимают этот код. К сожалению, это несостоятельно, как можно видеть в следующем примере:
|
1
2
3
4
5
6
|
//Eiffel/Dart-like language with//broken covariance:void put(List<Object> list) { list.add(10);}put(ArrayList<String>()); |
Здесь мы передаем List<String> функции, принимающей List<Object> , которая пытается добавить Integer в список.
Java делает ту же ошибку с массивами. Следующий код компилируется:
|
1
2
3
4
5
|
//Java:void put(Object[] list) { list[0]=10;}put(new String[1]); |
Сбой во время выполнения с ArrayStoreException .
Разница в использовании сайта
Однако в Java используется другой подход для универсальных классов и типов интерфейсов. По умолчанию тип класса или интерфейса является инвариантным , то есть:
- присваивается
L<V>тогда и только тогда, когдаUточно такого же типа, как иV
Поскольку в большинстве случаев это крайне неудобно, Java поддерживает так называемую вариацию use-site , где:
-
L<U>присваиваетсяL<? extends V>L<? extends V>еслиUявляется подтипомV, и -
L<U>присваиваетсяL<? super V>L<? super V>еслиUявляется супертипомV
Уродливый синтаксис ? extends V ? extends V или ? super V ? super V называется подстановочным знаком . Мы также говорим, что:
-
L<? extends V>L<? extends V>является ковариантным вV, и что -
L<? super V>L<? super V>контравариантно вV
Поскольку подстановочная нотация в Java так безобразна, мы больше не будем ее использовать в этом обсуждении. Вместо этого мы напишем символы подстановки, используя ключевые слова для контравариантности и ковариации соответственно. Таким образом:
-
L<out V>является ковариантным вV, и -
L<in V>является контравариантным вV
Данный V называется границей подстановочного знака:
-
out V— ограниченный сверху подстановочный знак, аV— его верхняя граница, и -
in V— ограниченный снизу подстановочный знак, аV— его нижняя граница.
Теоретически, мы могли бы иметь подстановочный знак с верхней и нижней границами, например, L<out X in Y> .
Мы можем выразить несколько верхних границ или несколько нижних границ, используя тип пересечения, например, L<out U&V> или L<in U&V> .
Обратите внимание, что выражения типа L<out Anything> и L<in Nothing> относятся к одному и тому же типу, и этот тип является супертипом всех экземпляров L
Вы часто будете видеть, что люди ссылаются на подстановочные типы как на экзистенциальные типы . Под этим они понимают, что если я знаю, что list имеет тип List<out Object> :
|
1
|
List<out Object> list; |
Тогда я знаю, что существует неизвестный тип T , подтип Object , такой, что list имеет тип List<T> .
В качестве альтернативы мы можем взять более цейлонскую точку зрения и сказать, что List<out Object> является объединением всех типов List<T> где T является подтипом Object .
В системе с дисперсией использования сайта следующий код не компилируется:
|
1
2
3
4
5
|
void iterate(List<Object> list) { Iterator<Object> it = list.iterator(); ...}iterate(ArrayList<String>()); //error: List<String> not a List<Object> |
Но этот код делает:
|
1
2
3
4
5
|
void iterate(List<out Object> list) { Iterator<out Object> it = list.iterator(); ...}iterate(ArrayList<String>()); |
Правильно, этот код не компилируется:
|
1
2
3
4
|
void put(List<out Object> list) { list.add(10); //error: Integer is not a Nothing}put(ArrayList<String>()); |
Теперь мы у входа в кроличью нору. Чтобы интегрировать типы с подстановочными символами в систему типов, отвергая при этом необоснованный код, как в приведенном выше примере, нам нужен гораздо более сложный алгоритм для подстановки аргументов типа.
Участник печатает в вариации использования сайта
То есть, когда у нас есть универсальный тип, такой как List<T> , с методом void add(T element) , вместо простой замены Object на T , как мы делаем с обычными инвариантными типами, нам нужно рассмотреть дисперсию местоположение, в котором встречается параметр типа. В этом случае T встречается в противоположном расположении типа List , а именно как тип параметра метода. Сложный алгоритм, который я не буду здесь записывать, говорит нам, что мы должны заменить Nothing , нижний тип, в этом месте.
Теперь представьте, что в нашем интерфейсе List есть метод partition() с такой подписью:
|
1
2
3
4
|
interface List<T> { List<List<T>> partition(Integer length); ...} |
Каков возвращаемый тип partition() для List<out Y> ? Ну, не теряя точности, это:
|
1
|
List<in List<in Y out Nothing> out List<in Nothing out Y>> |
Уч.
Поскольку никто в здравом уме не хочет думать о таких типах, разумный язык отбрасывает некоторые из этих границ, оставляя что-то вроде этого:
|
1
|
List<out List<out Y>> |
Что смутно приемлемо. К сожалению, даже в этом очень простом случае мы уже далеко за пределами того, что программист может легко следить за тем, что делает проверщик типов.
Итак, вот суть того, почему я не доверяю дисперсии использования сайта:
- Сильный принцип в дизайне Цейлона заключается в том, что программист должен всегда иметь возможность воспроизвести рассуждения компилятора. Очень сложно рассуждать о некоторых сложных типах, возникающих из-за дисперсии сайта использования.
- Это имеет вирусный эффект: как только эти подстановочные знаки закрепляются в коде, они начинают распространяться, и довольно сложно вернуться к моим обычным инвариантным типам.
Декларация сайта отклонений
Гораздо более разумная альтернатива использованию-сайт-дисперсия — это объявление-сайт-дисперсия , где мы определяем дисперсию универсального типа при объявлении. Это система, которую мы используем на Цейлоне. В этой системе нам нужно разделить List на три интерфейса:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
interface List<out T> { Iterator<T> iterator(); List<List<T>> partition(Integer length); ...} interface ListMutator<in T> { void add(T element);} interface MutableList<T> satisfies List<T>&ListMutator<T> {} |
List объявлен ковариантным типом, ListMutator — контравариантным типом, а MutableList — инвариантным подтипом обоих типов.
Может показаться, что требование наличия нескольких интерфейсов является большим недостатком различий между сайтами объявлений, но часто оказывается полезным отделить мутацию от операций чтения, и:
- мутационные операции очень часто инвариантны, тогда как
- операции чтения очень часто являются ковариантными.
Теперь мы можем написать наши функции так:
|
01
02
03
04
05
06
07
08
09
10
|
void iterate(List<Object> list) { Iterator<Object> it = list.iterator(); ...}iterate(ArrayList<String>()); void put(ListMutator<Integer> list) { list.add(10);}put(ArrayList<String>()); //error: List<String> is not a ListMutator<Integer> |
Вы можете прочитать больше о декларации сайта дисперсии здесь .
Почему нам нужна вариация использования сайта на Цейлоне
К сожалению, в Java нет различий между сайтами объявлений, и чистое взаимодействие с Java очень важно для нас. Мне не нравится добавлять основные функции в систему типов нашего языка исключительно для целей взаимодействия с Java, и поэтому я несколько лет сопротивлялся добавлению подстановочных знаков в Ceylon. В итоге реальность и практичность победили, а мое упрямство проиграло. Итак, в Ceylon 1.1 теперь есть разница между сайтами использования с одинарными подстановочными знаками.
Я пытался сохранить эту функцию как можно более жестко ограниченной, с минимальным необходимым для приличного взаимодействия Java. Это означает, что, как в Java:
- нет двойных подстановочных знаков формы
List<in X out Y>и - тип с подстановочными символами не может встречаться в
extendsилиsatisfiesопределению класса или интерфейса.
Кроме того, в отличие от Java:
- не существует неявно ограниченных подстановочных знаков, верхние границы всегда должны быть записаны явно, и
- поддержка подстановочных знаков не поддерживается.
Захват подстановочного знака — очень умная особенность Java, которая использует «экзистенциальную» интерпретацию подстановочного типа. Дана универсальная функция, подобная этой:
|
1
|
List<T> unmodifiableList<T>(List<T> list) => ... : |
Java позволил бы мне вызывать unmodifiableList() , передавая подстановочный тип, такой как List<out Object> , возвращая другой подстановочный знак List<out Object> , полагая, что существует какой-то неизвестный X , подтип Object для которого вызов будет иметь типизированный тип. , То есть этот код считается хорошо типизированным, хотя тип List<out Object> не может быть назначен List<T> для любого T :
|
1
2
|
List<out Object> objects = .... ;List<out Object> unmodifiable = unmodifiableList(objects); |
В Java ошибки ввода, связанные с захватом подстановочных знаков, почти невозможно понять, так как они включают неизвестный и не подлежащий определению тип. Я не планирую добавлять поддержку захвата с подстановочными знаками на Цейлоне.
Попробуйте это
Разница в использовании сайта уже реализована и уже работает в Ceylon 1.1, которую вы можете получить из GitHub, если у вас супер-мотивация.
Несмотря на то, что основной мотивацией для этой функции было отличное взаимодействие с Java, будут и другие, надеюсь, редкие случаи, когда подстановочные знаки будут полезны. Это, однако, не указывает на какой-либо значительный сдвиг в нашем подходе. Мы продолжим использовать дисперсию декларации на сайте в Ceylon SDK, за исключением крайних случаев.
ОБНОВИТЬ:
Я только что понял, что забыл сказать спасибо Россу Тэйту за помощь в тонкостях алгоритма набора членов для использования дисперсии сайта. Очень хитрый материал, который Росс знает по макушке!
| Ссылка: | Почему я не доверяю подстановочным знакам и зачем они нам нужны в любом случае от нашего партнера по JCG Гэвина Кинга в блоге Ceylon Team . |