В любом языке программирования, который сочетает полиморфизм подтипа (ориентация объекта) с параметрическим полиморфизмом (обобщение), возникает вопрос о дисперсии . Предположим, у меня есть список строк, типа 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 . |