Загалом параметр коваріантного типу - це той, якому дозволено змінюватись у міру підтипу класу (як варіант, змінюється підтипом, отже, і префікс "co-"). Більш конкретно:
trait List[+A]
List[Int]
є підтипом, List[AnyVal]
тому що Int
є підтипом AnyVal
. Це означає, що ви можете надати примірник, List[Int]
коли List[AnyVal]
очікується значення типу . Це дійсно дуже інтуїтивно зрозумілий спосіб роботи дженериків, але виявляється, що він невідомий (порушує систему типу) при використанні в присутності змінних даних. Ось чому дженерики є інваріантними на Яві. Короткий приклад невмілості використання масивів Java (які помилково є коваріантними):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Ми просто призначили значення типу String
для масиву типів Integer[]
. З причин, які повинні бути очевидними, це погані новини. Система типу Java фактично дозволяє це робити під час компіляції. JVM буде "корисно" кинути час ArrayStoreException
виконання. Система типу Scala запобігає цій проблемі, оскільки параметр типу в 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'
. Англійською мовою це можна прочитати так:
Функція є підтипом іншої функції B , якщо типу параметра А є супертіп типу параметра В той час як тип повертається А є підтипом типу повертається 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
, яка заборонена компілятором. Кожного разу, коли у вас є мутабельність, вам потрібно мати якийсь мутатор, для якого потрібен параметр методу певного типу, який (разом з аксесуаром) передбачає інваріантність. Коваріація працює з незмінними даними, оскільки єдино можливою операцією є аксесуар, якому може бути наданий тип коваріантного повернення.
var
встановлено, покиval
це не так. Це та сама причина, по якій незмінні колекції Scala є коваріантними, але ті, що змінюються - ні.