Почему пример не компилируется, иначе как работает (co-, contra- и in-) дисперсия?
-
20-08-2019 - |
Вопрос
Следуя дальше от этот вопрос, может ли кто-нибудь объяснить следующее в Scala:
class Slot[+T] (var some: T) {
// DOES NOT COMPILE
// "COVARIANT parameter in CONTRAVARIANT position"
}
Я понимаю разницу между +T
и T
в объявлении типа (он компилируется, если я использую T
).Но тогда как на самом деле написать класс, который является ковариантным по своему параметру типа, не прибегая к созданию вещи непараметризованный?Как я могу гарантировать, что следующее может быть создано только с помощью экземпляра T
?
class Slot[+T] (var some: Object){
def get() = { some.asInstanceOf[T] }
}
Редактировать - теперь сводим это к следующему:
abstract class _Slot[+T, V <: T] (var some: V) {
def getT() = { some }
}
это все хорошо, но теперь у меня есть два параметра типа, где мне нужен только один.Я переформулирую вопрос таким образом:
Как я могу написать неизменяемый Slot
класс, который является ковариантный в своем роде?
ПРАВКА 2:Да!Я использовал var
и не val
.Вот что я хотел сделать:
class Slot[+T] (val some: T) {
}
Решение
В общем, a ковариантный параметр type - это тот, который может изменяться в меньшую сторону по мере того, как класс является подтипом (альтернативно, может изменяться в зависимости от подтипа, отсюда и префикс "co-").Более конкретно:
trait List[+A]
List[Int]
является подтипом List[AnyVal]
потому что Int
является подтипом AnyVal
.Это означает, что вы можете предоставить экземпляр List[Int]
когда значение типа List[AnyVal]
ожидается.Это действительно очень интуитивно понятный способ работы дженериков, но оказывается, что он несостоятелен (нарушает систему типов) при использовании при наличии изменяемых данных.Вот почему дженерики инвариантны в Java.Краткий пример необоснованности использования Java-массивов (которые ошибочно ковариантны):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Мы только что присвоили значение типа String
к массиву типа Integer[]
.По причинам, которые должны быть очевидны, это плохая новость.Система типов Java фактически допускает это во время компиляции.JVM "услужливо" выдаст ArrayStoreException
во время выполнения.Система типов Scala предотвращает эту проблему, поскольку параметр type в Array
класс инвариантен (объявление является [A]
вместо того , чтобы [+A]
).
Обратите внимание, что существует другой тип дисперсии , известный как контравариантность.Это очень важно, поскольку объясняет, почему ковариация может вызвать некоторые проблемы.Контравариантность - это буквально противоположность ковариации:параметры варьируются вверх с подтипом.Это гораздо менее распространенный метод, отчасти потому, что он настолько нелогичен, хотя у него есть одно очень важное применение:функции.
trait Function1[-P, +R] {
def apply(p: P): R
}
Обратите внимание на "-" дисперсионная аннотация на P
введите параметр.Это заявление в целом означает, что Function1
является контравариантным в P
и ковариантен в R
.Таким образом, мы можем вывести следующие аксиомы:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Обратите внимание на это T1'
должен быть подтипом (или тем же типом) T1
, тогда как для T2
и T2'
.На английском языке это можно прочитать как следующее:
Функция A является подтипом другой функции B если тип параметра A является супертипом параметра type of B в то время как возвращаемый тип A является подтипом возвращаемого типа B.
Обоснование этого правила оставлено в качестве упражнения для читателя (подсказка:подумайте о разных случаях, поскольку функции имеют подтипы, как в моем примере с массивом выше).
С вашими новыми знаниями о со- и контравариантности вы должны быть в состоянии понять, почему следующий пример не будет скомпилирован:
trait List[+A] {
def cons(hd: A): List[A]
}
Проблема в том, что A
является ковариантным, в то время как cons
функция ожидает, что ее параметр типа будет равен инвариантный.Таким образом, A
меняет направление не в ту сторону.Достаточно интересно, что мы могли бы решить эту проблему, сделав List
контравариантный в A
, но тогда возвращаемый тип List[A]
было бы недействительным, поскольку cons
функция ожидает, что ее возвращаемый тип будет ковариантный.
У нас здесь только два варианта: а) сделать A
инвариантный, теряющий приятные, интуитивно понятные свойства подтипа ковариации, или б) добавить параметр локального типа в cons
метод, который определяет A
в качестве нижней границы:
def cons[B >: A](v: B): List[B]
Теперь это действительно так.Вы можете себе это представить A
меняется в сторону уменьшения, но B
способен изменяться в сторону увеличения по отношению к A
с тех пор как A
является его нижней границей.С помощью этого объявления метода мы можем иметь A
будьте ковариантны, и все получится.
Обратите внимание, что этот трюк работает только в том случае, если мы возвращаем экземпляр List
который специализируется на менее специфичном типе B
.Если вы попытаетесь сделать List
изменяемый, все рушится, так как в конечном итоге вы пытаетесь присвоить значения типа B
к переменной типа A
, который запрещен компилятором.Всякий раз, когда у вас есть изменчивость, вам нужно иметь какой-то мутатор, для которого требуется параметр метода определенного типа, который (вместе с средством доступа) подразумевает инвариантность.Ковариация работает с неизменяемыми данными, поскольку единственной возможной операцией является средство доступа, которому может быть присвоен ковариантный возвращаемый тип.
Другие советы
@Daniel объяснил это очень хорошо.Но если вкратце объяснить, если это было разрешено:
class Slot[+T](var some: T) {
def get: T = some
}
val slot: Slot[Dog] = new Slot[Dog](new Dog)
val slot2: Slot[Animal] = slot //because of co-variance
slot2.some = new Animal //legal as some is a var
slot.get ??
slot.get
затем выдаст ошибку во время выполнения, так как не удалось преобразовать Animal
к Dog
(да!).
В общем, изменчивость не сочетается с ковариацией и контрвариантностью.Именно по этой причине все коллекции Java инвариантны.
Видеть Скала на примере, стр. 57+ для полного обсуждения этого вопроса.
Если я правильно понимаю ваш комментарий, вам нужно перечитать отрывок, начиная с нижней части страницы 56 (по сути, то, о чем, я думаю, вы просите, не является типобезопасным без проверок во время выполнения, чего Scala не делает, так что вам не повезло).Перевод их примера для использования вашей конструкции:
val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2)) // Works, but now x.get() will blow up
Если вы чувствуете, что я не понимаю ваш вопрос (вероятно), попробуйте добавить дополнительные пояснения/контекст к описанию проблемы, и я попробую еще раз.
В ответ на ваше редактирование:Неизменяемые слоты — это совсем другая ситуация...* :) * Надеюсь, приведенный выше пример помог.
Вам необходимо применить нижнюю границу для параметра.Мне трудно запомнить синтаксис, но я думаю, что это будет выглядеть примерно так:
class Slot[+T, V <: T](var some: V) {
//blah
}
Примеры Scala немного сложны для понимания, несколько конкретных примеров помогли бы.