X <: Y
означает, что параметр типа X
должен быть подтипом типа Y
. X >: Y
означает обратное, X
должен быть супертипом Y
(в обоих случаях X = Y
в порядке). Эта запись может противоречить интуиции, можно подумать, что собака - это больше, чем животное (точнее, в терминах программирования, больше услуг), но по той же причине это точнее: собак меньше, чем животных, типа Animal
содержит больше значений, чем тип Dog
, он содержит всех собак, а также всех страусов. Итак Animal
>: Dog
.
Что касается причины, по которой push
имеет эту подпись, я не уверен, что смогу объяснить ее лучше, чем страница, из которой взят пример, но позвольте мне попробовать.
Это начинается с дисперсии. +
в class Stack[+A]
означает, что Stack
равно covariant in A
. если X
является подтипом Y
, Stack[X]
будет подтипом Stack[Y]
. Стопка собак - это тоже стая животных. Для математически склонных, если кто-то видит Stack как функцию от типа к типу (X является типом, если вы передаете его в Stack, вы получаете Stack [X], который является другим типом), будучи ковариантным, это означает, что он увеличивается функция (с <:, отношение подтипа является порядком на типах). </p>
Это кажется правильным, но это не такой простой вопрос. Это не было бы так, с помощью процедуры push, которая изменяет ее, добавляя новый элемент, то есть
def push(a: A): Unit
(пример другой, push возвращает новый стек, оставляя this
без изменений). Конечно, Стек [Собака] должен принимать только собак, которых толкают в него. В противном случае, это больше не будет стая собак. Но если мы примем это как стаю животных, мы можем сделать
val dogs : Stack[Dog] = new Stack[Dog]
val animals : Stack[Animal] = dogs // if we say stack is covariant
animals.push(ostrich) // allowed, we can push anything in a stack of any.
val topDog: Dog = dogs.top // ostrich!
Ясно, что рассматривать этот стек как ковариантный нецелесообразно. Когда стек рассматривается как Stack[Animal]
, разрешается операция, которая не будет Stack[Dog]
. То, что было сделано здесь с помощью push, может быть сделано с любой процедурой, которая принимает A в качестве аргумента. Если универсальный класс помечен как ковариантный с помощью C [+ A], то A не может быть типом какого-либо аргумента какой-либо (публичной) подпрограммы C, и компилятор обеспечит это.
Но стек в примере другой. Мы бы получили def push(a: A): Stack[A]
. Если кто-то вызывает push
, он получает новый стек, а исходный стек остается неизменным, это все равно правильный стек [собака], что бы ни было сдано. Если мы сделаем
val newStack = dogs.push(ostrich)
dogs
все тот же и все еще Stack[Dog]
. Очевидно, newStack
нет. Также это не Stack[Ostrich]
, потому что он также содержит собак, которые были (и остаются) в исходной стопке. Но это было бы правильно Stack[Animal]
. Если кто-то толкает кошку, точнее было бы сказать, что это Stack[Mammal]
(хотя это и стая животных). Если нажать 12
, это будет только Stack[Any]
, единственный распространенный супертип Dog
и Integer
. Проблема в том, что компилятор не может знать, что этот вызов безопасен, и не допустит аргумент a: A
в def push(a: A): Stack[A]
, если Stack
помечен как ковариантный. Если бы он остановился там, ковариантный стек был бы бесполезен, потому что не было бы никакого способа поместить значения в него.
Подпись решает проблему:
def push[B >: A](elem: B): Stack[B]
Если B
является предком A
, то при добавлении B
получается Stack[B]
. Таким образом, добавление Mammal
к Stack[Dog]
дает Stack[Mammal]
, добавление животного дает Stack[Animal]
, что хорошо. Добавление собаки тоже хорошо, A>: A это правда.
Это хорошо, но кажется слишком ограничительным. Что если тип добавленного элемента не является предком A
? Например, что если это потомок, например, dogs.push(goldenRetriever)
. Нельзя брать B = GoldenRetriever
, у кого-то не GoldenRetriever >: Dog
, а наоборот. Тем не менее, можно взять B = Dog все в порядке. Если ожидается, что параметр elem будет типа Dog, мы можем, конечно, передать GoldenRetriever. Каждый получает стог B, все еще стог собак. И это правильно, что B = GoldenRetriever
не было разрешено. Результат был бы напечатан как Stack[GoldenRetriever]
, что было бы неправильно, потому что в стеке тоже могли быть ирландские сеттеры.
А как насчет страусов?Ну, Ostrich
не является ни супертипом, ни подтипом Dog
.Но так же, как можно добавить золотистого ретривера, потому что это собака, и можно добавить собаку, страус - это животное, и можно добавить животное.Итак, взяв B = Animal>: Dog работает, и поэтому при нажатии на страуса вы получаете Stack[Animal]
.
Создание стека ковариантного форсирования этой сигнатуры, более сложное, чем наивный push(a: A) : Stack[A]
.Но мы получаем процедуру, которая является абсолютно гибкой, можно добавить что угодно, не только A
, и, тем не менее, печатать результат с максимальной точностью.И фактическая реализация, за исключением объявлений типов, такая же, как была бы с push(a: A)
.