Ogólnie rzecz biorąc, kowariantny parametr typu to taki, który może zmieniać się w dół, gdy klasa jest poddawana podtypowi (alternatywnie, zmieniać się wraz z podtypem, stąd prefiks „co-”). Bardziej konkretnie:
trait List[+A]
List[Int]
jest podtypem List[AnyVal]
ponieważ Int
jest podtypem AnyVal
. Oznacza to, że możesz podać wystąpienie, List[Int]
gdy List[AnyVal]
oczekiwana jest wartość typu . Jest to naprawdę bardzo intuicyjny sposób działania typów generycznych, ale okazuje się, że jest on nieuzasadniony (łamie system typów), gdy jest używany w obecności zmiennych danych. Dlatego typy generyczne są niezmienne w Javie. Krótki przykład nieprawidłowości przy użyciu tablic Java (które są błędnie kowariantne):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Właśnie przypisaliśmy wartość typu String
do tablicy typu Integer[]
. Z powodów, które powinny być oczywiste, to zła wiadomość. System typów Javy faktycznie na to pozwala w czasie kompilacji. JVM „pomocnie” wyśle plik ArrayStoreException
w czasie wykonywania. System typów Scali zapobiega temu problemowi, ponieważ parametr typu w Array
klasie jest niezmienny (deklaracja jest [A]
raczej niż [+A]
).
Zauważ, że istnieje inny rodzaj wariancji zwany kontrawariancją . Jest to bardzo ważne, ponieważ wyjaśnia, dlaczego kowariancja może powodować pewne problemy. Kontrawariancja jest dosłownie przeciwieństwem kowariancji: parametry zmieniają się w górę wraz z podtypami. Jest o wiele mniej powszechny, częściowo dlatego, że jest tak sprzeczny z intuicją, chociaż ma jedną bardzo ważną aplikację: funkcje.
trait Function1[-P, +R] {
def apply(p: P): R
}
Zwróć uwagę na adnotację wariancji „ - ” w P
parametrze typu. Ta deklaracja jako całość oznacza, że Function1
jest sprzeczna P
i kowariantna w R
. W ten sposób możemy wyprowadzić następujące aksjomaty:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Zauważ, że T1'
musi to być podtyp (lub tego samego typu) T1
, podczas gdy jest odwrotnie w przypadku T2
i T2'
. W języku angielskim można to odczytać w następujący sposób:
Funkcja jest podtypem innej funkcji B , jeśli typ parametr A jest supertypem typu parametru B, podczas gdy typ powrót A jest podtypem typu powrotnej B .
Czytelnikowi pozostawiono powód tej reguły jako ćwiczenie (wskazówka: pomyśl o różnych przypadkach, ponieważ funkcje są podtytułowane, jak mój przykład tablicy z góry).
Mając nowo odkrytą wiedzę na temat współ- i kontrawariancji, powinieneś być w stanie zrozumieć, dlaczego następujący przykład nie zostanie skompilowany:
trait List[+A] {
def cons(hd: A): List[A]
}
Problem w tym, że A
jest kowariantny, podczas gdy cons
funkcja oczekuje, że jej parametr typu będzie niezmienny . W ten sposób A
zmienia zły kierunek. Co ciekawe, moglibyśmy rozwiązać ten problem, wprowadzając List
kontrawariantność w A
, ale wtedy typ zwracany List[A]
byłby nieprawidłowy, ponieważ cons
funkcja oczekuje, że jego typ zwracany będzie kowariantny .
Nasze jedyne dwie opcje to a) uczynić A
niezmienność, tracąc ładne, intuicyjne właściwości kowariancji podtypów, lub b) dodać lokalny parametr typu do cons
metody, która definiuje A
jako dolną granicę:
def cons[B >: A](v: B): List[B]
To jest teraz ważne. Możesz sobie wyobrazić, że A
zmienia się w dół, ale B
może się zmieniać w górę w odniesieniu do, A
ponieważ A
jest to jego dolna granica. Dzięki tej deklaracji metody możemy A
być kowariantnymi i wszystko się ułoży.
Zauważ, że ta sztuczka działa tylko wtedy, gdy zwrócimy instancję, List
która jest wyspecjalizowana w mniej konkretnym typie B
. Jeśli spróbujesz uczynić List
mutable, wszystko się psuje, ponieważ w końcu próbujesz przypisać wartości typu B
do zmiennej typu A
, co jest zabronione przez kompilator. Ilekroć masz zmienność, musisz mieć pewnego rodzaju mutator, który wymaga parametru metody określonego typu, co (wraz z akcesorium) implikuje niezmienność. Kowariancja działa z niezmiennymi danymi, ponieważ jedyną możliwą operacją jest akcesor, któremu można nadać kowariantny typ zwrotu.
var
to ustawić, podczas gdyval
nie. Z tego samego powodu niezmienne kolekcje scali są kowariantne, a zmienne - nie.