Загалом параметр коваріантного типу - це той, якому дозволено змінюватись у міру підтипу класу (як варіант, змінюється підтипом, отже, і префікс "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 є коваріантними, але ті, що змінюються - ні.