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ż Intjest 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 Stringdo 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 ArrayStoreExceptionw czasie wykonywania. System typów Scali zapobiega temu problemowi, ponieważ parametr typu w Arrayklasie 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 Pparametrze typu. Ta deklaracja jako całość oznacza, że Function1jest sprzeczna Pi 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 T2i 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 Ajest kowariantny, podczas gdy consfunkcja oczekuje, że jej parametr typu będzie niezmienny . W ten sposób Azmienia zły kierunek. Co ciekawe, moglibyśmy rozwiązać ten problem, wprowadzając Listkontrawariantność w A, ale wtedy typ zwracany List[A]byłby nieprawidłowy, ponieważ consfunkcja oczekuje, że jego typ zwracany będzie kowariantny .
Nasze jedyne dwie opcje to a) uczynić Aniezmienność, tracąc ładne, intuicyjne właściwości kowariancji podtypów, lub b) dodać lokalny parametr typu do consmetody, która definiuje Ajako dolną granicę:
def cons[B >: A](v: B): List[B]
To jest teraz ważne. Możesz sobie wyobrazić, że Azmienia się w dół, ale Bmoże się zmieniać w górę w odniesieniu do, Aponieważ Ajest to jego dolna granica. Dzięki tej deklaracji metody możemy Abyć kowariantnymi i wszystko się ułoży.
Zauważ, że ta sztuczka działa tylko wtedy, gdy zwrócimy instancję, Listktóra jest wyspecjalizowana w mniej konkretnym typie B. Jeśli spróbujesz uczynić Listmutable, wszystko się psuje, ponieważ w końcu próbujesz przypisać wartości typu Bdo 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.
varto ustawić, podczas gdyvalnie. Z tego samego powodu niezmienne kolekcje scali są kowariantne, a zmienne - nie.