Это никак не связано с дисперсией.
Вы объявляете stack2
как Stack[Fruit]
, другими словами, вы заявляете, что вам разрешено помещать что-либо в Stack
, который является Fruit
. Apple
- это (подтип) Fruit
, поэтому вы можете поместить Apple
в Stack
из Fruits
.
Это называется подтипом и не имеет ничего общего с дисперсией .
Давайте сделаем шаг назад: что на самом деле означает дисперсия?
Ну, дисперсия означает «изменить» (думать о таких словах, как «изменить» или «переменная»). co- означает «вместе» (думать о сотрудничестве, совместном обучении, совместном размещении), contra- означает «против» (думать о противоречии, контрразведке, противодействии повстанцам) , контрацептив), а in- означает «не связанный» или «не-» (думать о недобровольном, недоступном, непереносимом).
Итак, у нас есть «изменение», и это изменение может быть «вместе», «против» или «не связано». Что ж, для того, чтобы иметь связанные изменения, нам нужны две вещи, которые меняются, и они могут либо изменяться вместе (то есть, когда одна вещь изменяется, другая вещь также изменяется «в том же направлении»), они могут изменяются друг против друга (то есть, когда одна вещь изменяется, другая изменяется «в противоположном направлении»), или они могут быть не связаны (то есть, когда одна вещь изменяется, другая нет.)
И это все, что есть в математической концепции ковариации, контравариантности и инвариантности. Все, что нам нужно, - это две «вещи», некоторое понятие «изменения», и это изменение должно иметь некоторое понятие «направления».
Теперь это, конечно, очень абстрактно. В этом конкретном случае речь идет о контексте подтипирования и параметрического полиморфизма. Как это применимо здесь?
Ну, каковы наши две вещи? Когда у нас есть конструктор типа , такой как C[A]
, тогда две наши вещи:
- Аргумент типа
A
.
- сконструированный тип , который является результатом применения конструктора типа
C
к A
.
И каковы наши изменения с чувством направления? Это подтип !
Итак, теперь возникает вопрос: «Когда я изменяю A
на B
(вдоль одного из направлений подтипирования, т. Е. Делаю его подтипом или супертипом), то как C[A]
относится к C[B]
».
И снова есть три возможности:
- Ковариация :
A <: B
⇒ C[A] <: C[B]
: когда A
является подтипом B
, тогда C[A]
является подтипом C[B]
, в других словами, когда я изменяю A
по иерархии подтипов, то C[A]
меняется с A
в в том же направлении .
- Контравариантность :
A <: B
⇒ C[A] :> C[B]
: если A
является подтипом B
, то C[A]
является супертипом C[B]
, другими словами, когда я изменяю A
по иерархии подтипов, тогда C[A]
меняет против A
в противоположном направлении .
- Инвариантность : между
C[A]
и C[B]
нет отношения подтипов, ни подтип, ни супертип другого.
Есть два вопроса, которые вы могли бы задать себе сейчас:
- Почему это полезно?
- Какой из них правильный?
Это полезно по той же причине, что и подтипирование полезно. На самом деле, это просто подтип. Итак, если у вас есть язык, который имеет как подтип, так и параметрический полиморфизм, тогда важно знать, является ли один тип подтипом другого типа, и дисперсия говорит вам, является ли составной тип подтипом другого составного типа тот же конструктор, основанный на связи подтипов между аргументами типа.
Какой из них правильный, тем сложнее, но, к счастью, у нас есть мощный инструмент для анализа, когда подтип является подтипом другого типа: Принцип замещения Барбары Лисков говорит нам, что тип S
подтип типа T
IFF любой экземпляр T
может быть заменен экземпляром S
без изменения наблюдаемых желаемых свойств программы.
Давайте рассмотрим простойуниверсальный тип, функция.Функция имеет два параметра типа, один для ввода и один для вывода.(Здесь мы все упрощаем.) F[A, B]
- это функция, которая принимает аргумент типа A
и возвращает результат типа B
.
И теперь мы разыгрываем несколько сценариев,У меня есть какая-то операция O , которая хочет работать с функцией от Fruit
с до Mammal
с (да, я знаю, интересные оригинальные примеры!) LSP говорит, что я также должен быть в состоянии передатьв подтипе этой функции, и все должно работать.Допустим, F
были ковариантными в A
.Тогда я смогу передать функцию от Apple
с до Mammal
с.Но что происходит, когда O передает Orange
на F
?Это должно быть разрешено! O удалось передать Orange
в F[Fruit, Mammal]
, потому что Orange
является подтипом Fruit
.Но функция из Apple
s не знает, как обращаться с Orange
s, поэтому она взрывается.LSP говорит, что это должно работать, но это означает, что единственный вывод, который мы можем сделать, состоит в том, что наше предположение неверно: F[Apple, Mammal]
не является подтипом F[Fruit, Mammal]
, другими словами, F
не является ковариантным в A
.
Что если бы это было противоречиво?Что если мы передадим F[Food, Mammal]
в O ?Ну, O снова пытается передать Orange
, и это работает: Orange
- это Food
, поэтому F[Food, Mammal]
знает, как обращаться с Orange
s.Теперь мы можем заключить, что функции являются контравариантными на своих входах, то есть вы можете передать функцию, которая принимает более общий тип, в качестве входа для замены функции, которая принимает более ограниченный тип, и все будет работатьхорошо.
Теперь давайте посмотрим на вывод F
.Что бы произошло, если бы F
были контравариантны в B
, как это было в A
?Мы передаем F[Fruit, Animal]
O .Согласно LSP, если мы правы и функции являются противоположными по своему результату, ничего плохого не должно произойти.К сожалению, O вызывает метод getMilk
для результата F
, но F
только что вернул его Chicken
.К сожалению.Следовательно, функции не могут быть противоположными в своих выходах.
OTOH, что произойдет, если мы передадим F[Fruit, Cow]
?Все еще работает! O вызывает getMilk
на возвращенной корове, и это действительно дает молоко.Таким образом, похоже, что функции являются ковариантными в своих выходах.
И это общее правило, которое применяется к дисперсии:
- Это безопасно (в смысле LSP) длясделать
C[A]
ковариант в A
IFF A
используется только в качестве выхода. - Это безопасно (всмысл LSP) сделать
C[A]
контравариантным в A
IFF A
используется только в качестве входа. - Если
A
может использоваться как вход или выход, то C[A]
должен быть инвариантным в A
, в противном случае результат не является безопасным.
Именно поэтому дизайнеры C♯ решили повторно использовать уже существующие ключевые слова in
и out
для аннотаций отклонений и Kotlin использует те же ключевые слова .
Так, например, неизменяемые коллекции, как правило, могут быть ковариантными по типу элемента, поскольку они не позволяют помещать что-либо в коллекцию (вы можете только построитьct новая коллекция потенциально другого типа), но только для извлечения элементов.Итак, если я хочу получить список чисел, и кто-то передает мне список целых чисел, у меня все в порядке.
С другой стороны, подумайте о выходном потоке (таком как Logger
), где вы можете только поместить материал в , но не получить его. Для этого безопасно быть контравариантным. То есть если я ожидаю, что смогу печатать строки, и кто-то протягивает мне принтер, который может печатать любой объект, то он также может печатать строки, и я в порядке. Другими примерами являются функции сравнения (вы помещаете обобщенные значения в , вывод фиксируется как логическое значение, или перечисление, или целое число, или любой другой дизайн, выбранный вашим языком). Или предикаты, они имеют только общие входные данные, выходные данные всегда фиксируются как булевы.
Но, например, коллекции mutable , в которые можно как добавлять, так и извлекать вещи, безопасны только по типу, когда они инвариантны. Существует множество учебных пособий, подробно объясняющих, как нарушить безопасность типов в Java или C♯, например, с помощью их ковариантных изменяемых массивов.
Обратите внимание, однако, что не всегда очевидно, является ли тип входом или выходом, когда вы переходите к более сложным типам. Например, когда ваш параметр типа используется в качестве верхней или нижней границы члена абстрактного типа, или когда у вас есть метод, который принимает функцию, которая возвращает функцию, тип аргумента которой является вашим параметром типа.
Теперь вернемся к вашему вопросу: у вас есть только один стек. Вы никогда не спросите, является ли один стек подтипом другого стека. Таким образом, дисперсия не входит в игру в вашем примере.